From 310c7c4e65551be7cd7ea1e3d22b9117c0d8cb04 Mon Sep 17 00:00:00 2001 From: eggmanQQQ2 <3671373519@qq.com> Date: Mon, 7 Jul 2025 10:49:18 +0800 Subject: [PATCH] feat : library --- library/.gitignore | 3 + library/build.gradle | 134 + library/proguard-rules.pro | 21 + library/src/main/AndroidManifest.xml | 30 + .../adapters/BaseListRecyclerViewAdapter.java | 226 + .../animator/AbstractPathAnimator.java | 111 + .../chwl/library/animator/PathAnimator.java | 106 + .../chwl/library/annatation/ActLayoutRes.java | 15 + .../com/chwl/library/base/IMvpBaseView.java | 11 + .../com/chwl/library/base/PresenterEvent.java | 12 + .../base/factory/AbstractMvpPresenter.java | 145 + .../library/base/factory/BaseMvpProxy.java | 144 + .../library/base/factory/CreatePresenter.java | 18 + .../base/factory/PresenterMvpFactory.java | 19 + .../base/factory/PresenterMvpFactoryImpl.java | 49 + .../base/factory/PresenterProxyInterface.java | 34 + .../chwl/library/bindinglist/BaseItem.java | 35 + .../com/chwl/library/bindinglist/IItem.java | 13 + .../library/bindinglist/ItemViewHolder.java | 34 + .../library/bindinglist/MultiTypeAdapter.java | 353 + .../chwl/library/constants/ConstantsLib.java | 7 + .../library/coremanager/AbstractBaseCore.java | 36 + .../chwl/library/coremanager/CoreError.java | 257 + .../chwl/library/coremanager/CoreEvent.java | 23 + .../coremanager/CoreEventListener.java | 31 + .../library/coremanager/CoreException.java | 36 + .../chwl/library/coremanager/CoreFactory.java | 93 + .../chwl/library/coremanager/CoreManager.java | 432 + .../chwl/library/coremanager/IBaseCore.java | 12 + .../chwl/library/coremanager/ICoreClient.java | 12 + .../java/com/chwl/library/error/CrashCat.java | 121 + .../chwl/library/language/LanguageHelper.kt | 219 + .../list/AnimatedExpandableListView.java | 606 ++ .../chwl/library/list/ArrayListAdapter.java | 88 + .../BaseAnimatedExpandableListAdapter.java | 107 + .../com/chwl/library/list/BaseGroupItem.java | 81 + .../chwl/library/list/BaseListAdapter.java | 64 + .../com/chwl/library/list/BaseListItem.java | 61 + .../java/com/chwl/library/list/GroupItem.java | 11 + .../java/com/chwl/library/list/ListItem.java | 20 + .../list/NoScrollingLinearLayoutManager.java | 33 + .../com/chwl/library/list/ViewHolder.java | 18 + .../chwl/library/manager/TracingManager.java | 42 + .../com/chwl/library/net/rxnet/RxNet.java | 69 + .../library/net/rxnet/callback/CallBack.java | 24 + .../rxnet/converter/GsonConverterFactory.java | 79 + .../rxnet/converter/GsonConverterPlugins.java | 29 + .../converter/GsonRequestBodyConverter.java | 55 + .../converter/GsonResponseBodyConverter.java | 54 + .../net/rxnet/exception/ApiException.kt | 20 + .../net/rxnet/exception/ErrorStatus.kt | 37 + .../net/rxnet/exception/ExceptionHandle.kt | 58 + .../net/rxnet/factory/Tls12SocketFactory.java | 68 + .../library/net/rxnet/https/HttpsUtils.java | 179 + .../interceptor/HttpCacheInterceptor.java | 55 + .../interceptor/HttpLoggingInterceptor.java | 328 + .../net/rxnet/manager/CacheManager.java | 39 + .../net/rxnet/manager/RxNetManager.java | 222 + .../library/net/rxnet/model/HttpHeaders.java | 271 + .../net/rxnet/utils/NetworkLatencyChecker.kt | 137 + .../library/net/rxnet/utils/RxFileUtils.java | 51 + .../library/net/rxnet/utils/RxNetLog.java | 38 + .../net/rxnet/utils/RxNetWorkUtils.java | 38 + .../record/AuditRecorderConfiguration.java | 139 + .../chwl/library/record/ExtAudioRecorder.java | 514 ++ .../com/chwl/library/record/FailRecorder.java | 27 + .../library/record/SimpleRecordFailed.java | 8 + .../com/chwl/library/rx/RxRetryWithDelay.java | 46 + .../java/com/chwl/library/rxbus/RxBus.java | 61 + .../com/chwl/library/rxbus/RxBusHelper.java | 97 + .../chwl/library/service/ErBanService.java | 35 + .../softinput/SoftHideKeyBoardUtil.java | 58 + .../swipeactivity/SwipeActivityUtils.java | 105 + .../swipeactivity/SwipeBackActivity.java | 107 + .../swipeactivity/SwipeBackActivityBase.java | 24 + .../SwipeBackActivityHelper.java | 59 + .../swipeactivity/SwipeBackLayout.java | 611 ++ .../library/swipeactivity/ViewDragHelper.java | 1581 ++++ .../library/threadmgr/SchedulePolicy.java | 21 + .../chwl/library/threadmgr/SpeakPolicy.java | 21 + .../library/threadmgr/SpeakThreadFactory.java | 17 + .../library/threadmgr/ThreadPoolManager.java | 66 + .../chwl/library/utils/AppMetaDataUtil.java | 56 + .../com/chwl/library/utils/ArrayUtils.java | 5513 ++++++++++++ .../library/utils/BitmapLruCacheHelper.java | 112 + .../com/chwl/library/utils/BlankUtil.java | 75 + .../library/utils/CenterDrawableHelper.java | 58 + .../chwl/library/utils/CharSequenceUtils.java | 220 + .../com/chwl/library/utils/CharUtils.java | 539 ++ .../com/chwl/library/utils/CommonUtils.java | 14 + .../com/chwl/library/utils/DeviceUtils.java | 41 + .../chwl/library/utils/DeviceUuidFactory.java | 71 + .../main/java/com/chwl/library/utils/FP.java | 909 ++ .../com/chwl/library/utils/FormatUtils.java | 205 + .../java/com/chwl/library/utils/IOUtils.java | 27 + .../java/com/chwl/library/utils/ImeUtil.java | 63 + .../java/com/chwl/library/utils/JavaUtil.java | 255 + .../com/chwl/library/utils/ListUtils.java | 17 + .../chwl/library/utils/LogCallerUtils.java | 46 + .../java/com/chwl/library/utils/LogUtil.java | 18 + .../java/com/chwl/library/utils/MimeType.java | 9 + .../com/chwl/library/utils/NetworkUtils.java | 210 + .../com/chwl/library/utils/NullUtils.java | 23 + .../com/chwl/library/utils/ObjectUtils.java | 837 ++ .../java/com/chwl/library/utils/PathHelper.kt | 147 + .../java/com/chwl/library/utils/ResUtil.java | 51 + .../chwl/library/utils/ResolutionUtils.java | 203 + .../library/utils/SafeDispatchHandler.java | 42 + .../com/chwl/library/utils/ScreenUtils.java | 260 + .../chwl/library/utils/SingleToastUtil.java | 51 + .../com/chwl/library/utils/SizeUtils.java | 176 + .../com/chwl/library/utils/StringUtils.java | 7569 +++++++++++++++++ .../com/chwl/library/utils/SystemUtils.java | 55 + .../library/utils/TextWatcherWrapper.java | 21 + .../com/chwl/library/utils/TimeUtils.java | 1000 +++ .../java/com/chwl/library/utils/UIUtils.java | 196 + .../java/com/chwl/library/utils/UUIDUtil.java | 13 + .../java/com/chwl/library/utils/UriUtil.java | 86 + .../java/com/chwl/library/utils/Validate.java | 1226 +++ .../com/chwl/library/utils/VersionUtil.java | 231 + .../chwl/library/utils/anim/AnimUtils.java | 53 + .../utils/anim/PauseableAnimManager.java | 115 + .../library/utils/asynctask/AsyncTask.java | 45 + .../utils/asynctask/ScheduledTask.java | 68 + .../com/chwl/library/utils/cache/Cache.java | 70 + .../chwl/library/utils/cache/CacheClient.java | 326 + .../utils/cache/CacheClientFactory.java | 69 + .../library/utils/cache/CacheException.java | 30 + .../library/utils/cache/CacheManager.java | 168 + .../library/utils/cache/ErrorCallback.java | 12 + .../utils/cache/NoSuchKeyException.java | 19 + .../library/utils/cache/ReturnCallback.java | 6 + .../library/utils/cache/StringDiskCache.java | 350 + .../library/utils/cache/StringLruCache.java | 311 + .../chwl/library/utils/codec/Base64Utils.java | 739 ++ .../library/utils/codec/CipherHelper.java | 186 + .../chwl/library/utils/codec/DES3Utils.java | 66 + .../chwl/library/utils/codec/DESUtils.java | 37 + .../chwl/library/utils/codec/MD5Utils.java | 105 + .../chwl/library/utils/codec/RC4Utils.java | 105 + .../chwl/library/utils/codec/SHAUtils.java | 66 + .../library/utils/config/BasicConfig.java | 285 + .../utils/constant/PackageNameConstants.kt | 12 + .../library/utils/file/BasicFileUtils.java | 513 ++ .../chwl/library/utils/file/CMDExecute.java | 39 + .../utils/file/CustomFileException.java | 11 + .../utils/file/FileNoSpaceException.java | 11 + .../chwl/library/utils/file/JXFileUtils.java | 1046 +++ .../chwl/library/utils/file/StorageUtils.java | 536 ++ .../file/TargetNotPreparedException.java | 11 + .../com/chwl/library/utils/image/Blur.java | 287 + .../library/utils/image/JXImageUtils.java | 850 ++ .../chwl/library/utils/json/JsonParser.java | 101 + .../chwl/library/utils/json/JsonUtils.java | 51 + .../AutoActivityLifecycleCallback.java | 57 + .../library/utils/keyboard/KeyboardUtil.java | 90 + .../keyboard/KeyboardVisibilityEvent.java | 115 + .../KeyboardVisibilityEventListener.java | 6 + .../utils/keyboard/SimpleUnregister.java | 41 + .../library/utils/keyboard/Unregister.java | 13 + .../library/utils/log/FastDateFormat.java | 1660 ++++ .../chwl/library/utils/log/FormatCache.java | 223 + .../com/chwl/library/utils/log/LogToES.java | 347 + .../java/com/chwl/library/utils/log/MLog.java | 834 ++ .../library/utils/net/MobileNumberUtils.java | 70 + .../library/utils/net/NetworkMonitor.java | 98 + .../chwl/library/utils/pref/CommonPref.java | 31 + .../chwl/library/utils/pref/ObjectPref.java | 27 + .../chwl/library/utils/pref/SettingsPref.java | 42 + .../chwl/library/utils/pref/XSharedPref.java | 327 + .../chwl/library/utils/pref/YSharedPref.java | 198 + .../library/utils/valid/Validateable.java | 6 + .../chwl/library/utils/valid/Validation.java | 218 + .../widget/DrawableCenterTextView.java | 40 + .../chwl/library/widget/IOSSwitchView.java | 571 ++ .../java/com/chwl/library/widget/SVGAView.kt | 435 + .../library/widget/ShapeConstrainLayout.java | 72 + .../com/chwl/library/widget/ViewItem.java | 91 + .../RowColumnDifItemDecoration.java | 64 + .../library/widget/drag/ViewDragCallback.kt | 63 + .../library/widget/drag/ViewDragLayout.kt | 136 + .../widget/tab/BaseTabTitleProvider.kt | 7 + .../tab/SmartTabIndicationInterpolator.java | 96 + .../library/widget/tab/SmartTabLayout.java | 681 ++ .../library/widget/tab/SmartTabLayout2.java | 709 ++ .../library/widget/tab/SmartTabStrip.java | 446 + .../library/widget/tab/SmartTabStrip2.java | 446 + .../chwl/library/widget/tab/TabColorizer.java | 19 + .../library/widget/tab/TabTitleProvider.java | 8 + .../com/chwl/library/widget/tab/Utils.java | 120 + .../widget/tab/util/FragmentPageAdapter.kt | 35 + .../library/widget/tab/util/PagerItem.java | 38 + .../library/widget/tab/util/PagerItems.java | 34 + .../widget/tab/util/ViewPagerItem.java | 45 + .../widget/tab/util/ViewPagerItemAdapter.java | 83 + .../widget/tab/util/ViewPagerItems.java | 64 + .../library/widget/tab/util/v4/Bundler.java | 520 ++ .../widget/tab/util/v4/FragmentPagerItem.java | 76 + .../tab/util/v4/FragmentPagerItemAdapter.java | 84 + .../tab/util/v4/FragmentPagerItems.java | 81 + .../v4/FragmentStatePagerItemAdapter.java | 84 + .../library/widget/text/DrawableTextView.java | 791 ++ .../xlist/FamiliarDefaultItemDecoration.java | 569 ++ .../widget/xlist/FamiliarRecyclerView.java | 503 ++ .../svgaplayer/IClickAreaListener.kt | 9 + .../com/opensource/svgaplayer/SVGACache.kt | 119 + .../com/opensource/svgaplayer/SVGACallback.kt | 13 + .../svgaplayer/SVGAClickAreaListener.kt | 9 + .../com/opensource/svgaplayer/SVGADrawable.kt | 106 + .../svgaplayer/SVGADynamicEntity.kt | 153 + .../opensource/svgaplayer/SVGAImageView.kt | 329 + .../com/opensource/svgaplayer/SVGAParser.kt | 565 ++ .../com/opensource/svgaplayer/SVGAPlayer.kt | 19 + .../opensource/svgaplayer/SVGASoundManager.kt | 194 + .../opensource/svgaplayer/SVGAVideoEntity.kt | 345 + .../bitmap/BitmapSampleSizeCalculator.kt | 33 + .../bitmap/SVGABitmapByteArrayDecoder.kt | 16 + .../svgaplayer/bitmap/SVGABitmapDecoder.kt | 35 + .../bitmap/SVGABitmapFileDecoder.kt | 16 + .../svgaplayer/drawer/SGVADrawer.kt | 53 + .../svgaplayer/drawer/SVGACanvasDrawer.kt | 569 ++ .../svgaplayer/entities/SVGAAudioEntity.kt | 24 + .../svgaplayer/entities/SVGAPathEntity.kt | 100 + .../entities/SVGAVideoShapeEntity.kt | 356 + .../entities/SVGAVideoSpriteEntity.kt | 60 + .../entities/SVGAVideoSpriteFrameEntity.kt | 94 + .../svgaplayer/proto/AudioEntity.java | 255 + .../svgaplayer/proto/FrameEntity.java | 256 + .../opensource/svgaplayer/proto/Layout.java | 202 + .../svgaplayer/proto/MovieEntity.java | 263 + .../svgaplayer/proto/MovieParams.java | 226 + .../svgaplayer/proto/ShapeEntity.java | 1500 ++++ .../svgaplayer/proto/SpriteEntity.java | 200 + .../svgaplayer/proto/Transform.java | 248 + .../com/opensource/svgaplayer/utils/Pools.kt | 102 + .../svgaplayer/utils/SVGAScaleInfo.kt | 146 + .../svgaplayer/utils/SVGAStructs.kt | 11 + .../svgaplayer/utils/log/DefaultLogCat.kt | 28 + .../svgaplayer/utils/log/ILogger.kt | 12 + .../svgaplayer/utils/log/LogUtils.kt | 57 + .../svgaplayer/utils/log/SVGALogger.kt | 40 + .../drawable-v24/ic_launcher_foreground.xml | 23 + .../res/drawable-xhdpi/ic_decoration.webp | Bin 0 -> 1590 bytes .../res/drawable-xhdpi/shadow_bottom.webp | Bin 0 -> 212 bytes .../main/res/drawable-xhdpi/shadow_left.webp | Bin 0 -> 176 bytes .../main/res/drawable-xhdpi/shadow_right.webp | Bin 0 -> 174 bytes .../drawable-xhdpi/user_ucrop_ic_closs.webp | Bin 0 -> 497 bytes .../drawable-xhdpi/user_ucrop_ic_sure.webp | Bin 0 -> 473 bytes .../drawable/shape_dialog_btn_white_bg.xml | 6 + .../main/res/layout/layout_user_card_item.xml | 42 + .../src/main/res/layout/swipeback_layout.xml | 7 + library/src/main/res/values-ar/strings.xml | 130 + .../src/main/res/values-zh-rTW/strings.xml | 127 + library/src/main/res/values/arrays.xml | 17 + library/src/main/res/values/attrs.xml | 365 + library/src/main/res/values/colors.xml | 5 + library/src/main/res/values/dimens.xml | 21 + library/src/main/res/values/integers.xml | 7 + library/src/main/res/values/strings.xml | 126 + library/src/main/res/values/styles.xml | 11 + .../com/chwl/library/common/Constants.java | 10 + .../com/chwl/library/common/SpConstants.kt | 13 + .../library/common/application/BaseApp.java | 41 + .../chwl/library/common/application/Env.java | 146 + .../common/application/IAppLifeCycle.java | 12 + .../library/common/base/BaseActivity.java | 211 + .../common/base/BaseCommonDialogFragment.java | 234 + .../library/common/base/BaseLinearLayout.java | 165 + .../chwl/library/common/base/BaseViewTag.java | 119 + .../library/common/delegate/SpDelegate.kt | 50 + .../library/common/entity/CommonTabEntity.kt | 10 + .../chwl/library/common/file/FileHelper.java | 882 ++ .../fragmentation/ISupportActivity.java | 13 + .../fragmentation/ISupportFragment.java | 36 + .../SupportActivityDelegate.java | 99 + .../SupportFragmentDelegate.java | 172 + .../common/fragmentation/SupportHelper.java | 209 + .../fragmentation/TransactionDelegate.java | 305 + .../fragmentation/internal/ResultRecord.java | 48 + .../internal/TransactionRecord.java | 9 + .../common/fragmentation/queue/Action.java | 35 + .../fragmentation/queue/ActionQueue.java | 81 + .../windowcallback/IWindowCallbackProxy.kt | 14 + .../windowcallback/WindowCallbackProxyUtil.kt | 25 + .../library/common/glide/AnimEffectUtil.kt | 434 + .../chwl/library/common/glide/GlideUtils.kt | 1592 ++++ .../transform/AssignScaleTransformation.kt | 96 + .../transform/ComplexTransformation.java | 152 + .../library/common/util/ActivityHelper.java | 48 + .../chwl/library/common/util/AlbumUtils.kt | 313 + .../chwl/library/common/util/ClickUtils.kt | 76 + .../com/chwl/library/common/util/Config.java | 195 + .../chwl/library/common/util/CoreUtils.java | 59 + .../chwl/library/common/util/DeviceUtil.kt | 336 + .../chwl/library/common/util/DoubleUtils.java | 103 + .../library/common/util/ExecutorCenter.java | 57 + .../chwl/library/common/util/LibLogger.java | 719 ++ .../library/common/util/LimitClickUtils.java | 62 + .../com/chwl/library/common/util/MMKVExt.kt | 120 + .../com/chwl/library/common/util/OtherExt.kt | 371 + .../library/common/util/PhotoCompressUtil.kt | 182 + .../com/chwl/library/common/util/Pools.java | 36 + .../library/common/util/ReflectionUtils.java | 40 + .../com/chwl/library/common/util/SPUtils.java | 107 + .../common/util/ScheduledExecutor.java | 11 + .../common/util/ScheduledExecutorAdapter.java | 34 + .../common/util/SoftKeyboardStateHelper.java | 89 + .../com/chwl/library/common/util/Utils.java | 178 + .../library/common/util/ViewBindingUtil.kt | 97 + .../widget/LinesFlexBoxLayoutManager.java | 56 + .../library/common/widget/VpRecyclerView.kt | 65 + .../module_common/res/values-ar/strings.xml | 7 + .../res/values-zh-rTW/strings.xml | 7 + library/src/module_common/res/values/ids.xml | 6 + .../src/module_common/res/values/strings.xml | 7 + .../AfterPermissionGranted.java | 29 + .../easypermisssion/AppSettingsDialog.java | 358 + .../AppSettingsDialogHolderActivity.java | 65 + .../easypermisssion/EasyPermissions.java | 358 + .../easypermisssion/PermissionRequest.java | 260 + .../RationaleDialogClickListener.java | 77 + .../RationaleDialogConfig.java | 143 + .../RationaleDialogFragment.java | 113 + .../RationaleDialogFragmentCompat.java | 97 + .../helper/ActivityPermissionHelper.java | 59 + .../AppCompatActivityPermissionsHelper.java | 37 + .../helper/BaseSupportPermissionsHelper.java | 45 + .../helper/LowApiPermissionsHelper.java | 47 + .../helper/PermissionHelper.java | 113 + .../SupportFragmentPermissionHelper.java | 36 + .../easypermisssion/helper/package-info.java | 4 + .../res/drawable/bg_common_cancel.xml | 6 + .../res/drawable/bg_common_confirm_normal.xml | 6 + .../res/drawable/shape_white_20dp_round.xml | 8 + .../layout_permission_rationale_dialog.xml | 53 + .../res/values-ar/strings.xml | 7 + .../res/values-zh-rTW/strings.xml | 7 + .../res/values/colors.xml | 10 + .../res/values/strings.xml | 7 + .../res/values/styles.xml | 29 + .../java/com/chwl/library/luban/Checker.java | 224 + .../library/luban/CompressionPredicate.java | 20 + .../java/com/chwl/library/luban/Engine.java | 128 + .../library/luban/InputStreamProvider.java | 16 + .../java/com/chwl/library/luban/Luban.java | 397 + .../library/luban/OnCompressListener.java | 21 + .../chwl/library/luban/OnRenameListener.java | 22 + 347 files changed, 67175 insertions(+) create mode 100644 library/.gitignore create mode 100644 library/build.gradle create mode 100644 library/proguard-rules.pro create mode 100644 library/src/main/AndroidManifest.xml create mode 100644 library/src/main/java/com/chwl/library/adapters/BaseListRecyclerViewAdapter.java create mode 100644 library/src/main/java/com/chwl/library/animator/AbstractPathAnimator.java create mode 100644 library/src/main/java/com/chwl/library/animator/PathAnimator.java create mode 100644 library/src/main/java/com/chwl/library/annatation/ActLayoutRes.java create mode 100644 library/src/main/java/com/chwl/library/base/IMvpBaseView.java create mode 100644 library/src/main/java/com/chwl/library/base/PresenterEvent.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/AbstractMvpPresenter.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/BaseMvpProxy.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/CreatePresenter.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactory.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactoryImpl.java create mode 100644 library/src/main/java/com/chwl/library/base/factory/PresenterProxyInterface.java create mode 100644 library/src/main/java/com/chwl/library/bindinglist/BaseItem.java create mode 100644 library/src/main/java/com/chwl/library/bindinglist/IItem.java create mode 100644 library/src/main/java/com/chwl/library/bindinglist/ItemViewHolder.java create mode 100644 library/src/main/java/com/chwl/library/bindinglist/MultiTypeAdapter.java create mode 100644 library/src/main/java/com/chwl/library/constants/ConstantsLib.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/AbstractBaseCore.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreError.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreEvent.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreEventListener.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreException.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreFactory.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/CoreManager.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/IBaseCore.java create mode 100644 library/src/main/java/com/chwl/library/coremanager/ICoreClient.java create mode 100644 library/src/main/java/com/chwl/library/error/CrashCat.java create mode 100644 library/src/main/java/com/chwl/library/language/LanguageHelper.kt create mode 100644 library/src/main/java/com/chwl/library/list/AnimatedExpandableListView.java create mode 100644 library/src/main/java/com/chwl/library/list/ArrayListAdapter.java create mode 100644 library/src/main/java/com/chwl/library/list/BaseAnimatedExpandableListAdapter.java create mode 100644 library/src/main/java/com/chwl/library/list/BaseGroupItem.java create mode 100644 library/src/main/java/com/chwl/library/list/BaseListAdapter.java create mode 100644 library/src/main/java/com/chwl/library/list/BaseListItem.java create mode 100644 library/src/main/java/com/chwl/library/list/GroupItem.java create mode 100644 library/src/main/java/com/chwl/library/list/ListItem.java create mode 100644 library/src/main/java/com/chwl/library/list/NoScrollingLinearLayoutManager.java create mode 100644 library/src/main/java/com/chwl/library/list/ViewHolder.java create mode 100644 library/src/main/java/com/chwl/library/manager/TracingManager.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/RxNet.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/callback/CallBack.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterFactory.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterPlugins.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/converter/GsonRequestBodyConverter.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/converter/GsonResponseBodyConverter.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/exception/ApiException.kt create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/exception/ErrorStatus.kt create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/exception/ExceptionHandle.kt create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/factory/Tls12SocketFactory.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/https/HttpsUtils.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpCacheInterceptor.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpLoggingInterceptor.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/manager/CacheManager.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/manager/RxNetManager.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/model/HttpHeaders.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/utils/NetworkLatencyChecker.kt create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/utils/RxFileUtils.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetLog.java create mode 100644 library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetWorkUtils.java create mode 100644 library/src/main/java/com/chwl/library/record/AuditRecorderConfiguration.java create mode 100644 library/src/main/java/com/chwl/library/record/ExtAudioRecorder.java create mode 100644 library/src/main/java/com/chwl/library/record/FailRecorder.java create mode 100644 library/src/main/java/com/chwl/library/record/SimpleRecordFailed.java create mode 100644 library/src/main/java/com/chwl/library/rx/RxRetryWithDelay.java create mode 100644 library/src/main/java/com/chwl/library/rxbus/RxBus.java create mode 100644 library/src/main/java/com/chwl/library/rxbus/RxBusHelper.java create mode 100644 library/src/main/java/com/chwl/library/service/ErBanService.java create mode 100644 library/src/main/java/com/chwl/library/softinput/SoftHideKeyBoardUtil.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/SwipeActivityUtils.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivity.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityBase.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityHelper.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/SwipeBackLayout.java create mode 100644 library/src/main/java/com/chwl/library/swipeactivity/ViewDragHelper.java create mode 100644 library/src/main/java/com/chwl/library/threadmgr/SchedulePolicy.java create mode 100644 library/src/main/java/com/chwl/library/threadmgr/SpeakPolicy.java create mode 100644 library/src/main/java/com/chwl/library/threadmgr/SpeakThreadFactory.java create mode 100644 library/src/main/java/com/chwl/library/threadmgr/ThreadPoolManager.java create mode 100644 library/src/main/java/com/chwl/library/utils/AppMetaDataUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/ArrayUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/BitmapLruCacheHelper.java create mode 100644 library/src/main/java/com/chwl/library/utils/BlankUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/CenterDrawableHelper.java create mode 100644 library/src/main/java/com/chwl/library/utils/CharSequenceUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/CharUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/CommonUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/DeviceUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/DeviceUuidFactory.java create mode 100644 library/src/main/java/com/chwl/library/utils/FP.java create mode 100644 library/src/main/java/com/chwl/library/utils/FormatUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/IOUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/ImeUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/JavaUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/ListUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/LogCallerUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/LogUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/MimeType.java create mode 100644 library/src/main/java/com/chwl/library/utils/NetworkUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/NullUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/ObjectUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/PathHelper.kt create mode 100644 library/src/main/java/com/chwl/library/utils/ResUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/ResolutionUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/SafeDispatchHandler.java create mode 100644 library/src/main/java/com/chwl/library/utils/ScreenUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/SingleToastUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/SizeUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/StringUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/SystemUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/TextWatcherWrapper.java create mode 100644 library/src/main/java/com/chwl/library/utils/TimeUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/UIUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/UUIDUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/UriUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/Validate.java create mode 100644 library/src/main/java/com/chwl/library/utils/VersionUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/anim/AnimUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/anim/PauseableAnimManager.java create mode 100644 library/src/main/java/com/chwl/library/utils/asynctask/AsyncTask.java create mode 100644 library/src/main/java/com/chwl/library/utils/asynctask/ScheduledTask.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/Cache.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/CacheClient.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/CacheClientFactory.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/CacheException.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/CacheManager.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/ErrorCallback.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/NoSuchKeyException.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/ReturnCallback.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/StringDiskCache.java create mode 100644 library/src/main/java/com/chwl/library/utils/cache/StringLruCache.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/Base64Utils.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/CipherHelper.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/DES3Utils.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/DESUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/MD5Utils.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/RC4Utils.java create mode 100644 library/src/main/java/com/chwl/library/utils/codec/SHAUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/config/BasicConfig.java create mode 100644 library/src/main/java/com/chwl/library/utils/constant/PackageNameConstants.kt create mode 100644 library/src/main/java/com/chwl/library/utils/file/BasicFileUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/CMDExecute.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/CustomFileException.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/FileNoSpaceException.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/JXFileUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/StorageUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/file/TargetNotPreparedException.java create mode 100644 library/src/main/java/com/chwl/library/utils/image/Blur.java create mode 100644 library/src/main/java/com/chwl/library/utils/image/JXImageUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/json/JsonParser.java create mode 100644 library/src/main/java/com/chwl/library/utils/json/JsonUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/AutoActivityLifecycleCallback.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/KeyboardUtil.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEvent.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEventListener.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/SimpleUnregister.java create mode 100644 library/src/main/java/com/chwl/library/utils/keyboard/Unregister.java create mode 100644 library/src/main/java/com/chwl/library/utils/log/FastDateFormat.java create mode 100644 library/src/main/java/com/chwl/library/utils/log/FormatCache.java create mode 100644 library/src/main/java/com/chwl/library/utils/log/LogToES.java create mode 100644 library/src/main/java/com/chwl/library/utils/log/MLog.java create mode 100644 library/src/main/java/com/chwl/library/utils/net/MobileNumberUtils.java create mode 100644 library/src/main/java/com/chwl/library/utils/net/NetworkMonitor.java create mode 100644 library/src/main/java/com/chwl/library/utils/pref/CommonPref.java create mode 100644 library/src/main/java/com/chwl/library/utils/pref/ObjectPref.java create mode 100644 library/src/main/java/com/chwl/library/utils/pref/SettingsPref.java create mode 100644 library/src/main/java/com/chwl/library/utils/pref/XSharedPref.java create mode 100644 library/src/main/java/com/chwl/library/utils/pref/YSharedPref.java create mode 100644 library/src/main/java/com/chwl/library/utils/valid/Validateable.java create mode 100644 library/src/main/java/com/chwl/library/utils/valid/Validation.java create mode 100644 library/src/main/java/com/chwl/library/widget/DrawableCenterTextView.java create mode 100644 library/src/main/java/com/chwl/library/widget/IOSSwitchView.java create mode 100644 library/src/main/java/com/chwl/library/widget/SVGAView.kt create mode 100644 library/src/main/java/com/chwl/library/widget/ShapeConstrainLayout.java create mode 100644 library/src/main/java/com/chwl/library/widget/ViewItem.java create mode 100644 library/src/main/java/com/chwl/library/widget/decoration/RowColumnDifItemDecoration.java create mode 100644 library/src/main/java/com/chwl/library/widget/drag/ViewDragCallback.kt create mode 100644 library/src/main/java/com/chwl/library/widget/drag/ViewDragLayout.kt create mode 100644 library/src/main/java/com/chwl/library/widget/tab/BaseTabTitleProvider.kt create mode 100644 library/src/main/java/com/chwl/library/widget/tab/SmartTabIndicationInterpolator.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout2.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip2.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/TabColorizer.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/TabTitleProvider.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/Utils.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/FragmentPageAdapter.kt create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/PagerItem.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/PagerItems.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItem.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItemAdapter.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItems.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/v4/Bundler.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItem.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItemAdapter.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItems.java create mode 100644 library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentStatePagerItemAdapter.java create mode 100644 library/src/main/java/com/chwl/library/widget/text/DrawableTextView.java create mode 100644 library/src/main/java/com/chwl/library/widget/xlist/FamiliarDefaultItemDecoration.java create mode 100644 library/src/main/java/com/chwl/library/widget/xlist/FamiliarRecyclerView.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGACache.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGACallback.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGAParser.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/Layout.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/proto/Transform.java create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/Pools.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt create mode 100644 library/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt create mode 100644 library/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 library/src/main/res/drawable-xhdpi/ic_decoration.webp create mode 100644 library/src/main/res/drawable-xhdpi/shadow_bottom.webp create mode 100644 library/src/main/res/drawable-xhdpi/shadow_left.webp create mode 100644 library/src/main/res/drawable-xhdpi/shadow_right.webp create mode 100644 library/src/main/res/drawable-xhdpi/user_ucrop_ic_closs.webp create mode 100644 library/src/main/res/drawable-xhdpi/user_ucrop_ic_sure.webp create mode 100644 library/src/main/res/drawable/shape_dialog_btn_white_bg.xml create mode 100644 library/src/main/res/layout/layout_user_card_item.xml create mode 100644 library/src/main/res/layout/swipeback_layout.xml create mode 100644 library/src/main/res/values-ar/strings.xml create mode 100644 library/src/main/res/values-zh-rTW/strings.xml create mode 100644 library/src/main/res/values/arrays.xml create mode 100644 library/src/main/res/values/attrs.xml create mode 100644 library/src/main/res/values/colors.xml create mode 100644 library/src/main/res/values/dimens.xml create mode 100644 library/src/main/res/values/integers.xml create mode 100644 library/src/main/res/values/strings.xml create mode 100644 library/src/main/res/values/styles.xml create mode 100644 library/src/module_common/java/com/chwl/library/common/Constants.java create mode 100644 library/src/module_common/java/com/chwl/library/common/SpConstants.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/application/BaseApp.java create mode 100644 library/src/module_common/java/com/chwl/library/common/application/Env.java create mode 100644 library/src/module_common/java/com/chwl/library/common/application/IAppLifeCycle.java create mode 100644 library/src/module_common/java/com/chwl/library/common/base/BaseActivity.java create mode 100644 library/src/module_common/java/com/chwl/library/common/base/BaseCommonDialogFragment.java create mode 100644 library/src/module_common/java/com/chwl/library/common/base/BaseLinearLayout.java create mode 100644 library/src/module_common/java/com/chwl/library/common/base/BaseViewTag.java create mode 100644 library/src/module_common/java/com/chwl/library/common/delegate/SpDelegate.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/entity/CommonTabEntity.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/file/FileHelper.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportActivity.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportFragment.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/SupportActivityDelegate.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/SupportFragmentDelegate.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/SupportHelper.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/TransactionDelegate.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/internal/ResultRecord.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/internal/TransactionRecord.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/queue/Action.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/queue/ActionQueue.java create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/IWindowCallbackProxy.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/WindowCallbackProxyUtil.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/glide/AnimEffectUtil.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/glide/GlideUtils.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/transform/AssignScaleTransformation.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/transform/ComplexTransformation.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ActivityHelper.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/AlbumUtils.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ClickUtils.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/Config.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/CoreUtils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/DeviceUtil.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/DoubleUtils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ExecutorCenter.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/LibLogger.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/LimitClickUtils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/MMKVExt.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/OtherExt.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/PhotoCompressUtil.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/util/Pools.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ReflectionUtils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/SPUtils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutor.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutorAdapter.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/SoftKeyboardStateHelper.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/Utils.java create mode 100644 library/src/module_common/java/com/chwl/library/common/util/ViewBindingUtil.kt create mode 100644 library/src/module_common/java/com/chwl/library/common/widget/LinesFlexBoxLayoutManager.java create mode 100644 library/src/module_common/java/com/chwl/library/common/widget/VpRecyclerView.kt create mode 100644 library/src/module_common/res/values-ar/strings.xml create mode 100644 library/src/module_common/res/values-zh-rTW/strings.xml create mode 100644 library/src/module_common/res/values/ids.xml create mode 100644 library/src/module_common/res/values/strings.xml create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/AfterPermissionGranted.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialog.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialogHolderActivity.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/EasyPermissions.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/PermissionRequest.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogClickListener.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogConfig.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragment.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragmentCompat.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/ActivityPermissionHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/AppCompatActivityPermissionsHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/BaseSupportPermissionsHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/LowApiPermissionsHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/PermissionHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/SupportFragmentPermissionHelper.java create mode 100644 library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/package-info.java create mode 100644 library/src/module_easypermission/res/drawable/bg_common_cancel.xml create mode 100644 library/src/module_easypermission/res/drawable/bg_common_confirm_normal.xml create mode 100644 library/src/module_easypermission/res/drawable/shape_white_20dp_round.xml create mode 100644 library/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml create mode 100644 library/src/module_easypermission/res/values-ar/strings.xml create mode 100644 library/src/module_easypermission/res/values-zh-rTW/strings.xml create mode 100644 library/src/module_easypermission/res/values/colors.xml create mode 100644 library/src/module_easypermission/res/values/strings.xml create mode 100644 library/src/module_easypermission/res/values/styles.xml create mode 100644 library/src/module_luban/java/com/chwl/library/luban/Checker.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/CompressionPredicate.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/Engine.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/InputStreamProvider.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/Luban.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/OnCompressListener.java create mode 100644 library/src/module_luban/java/com/chwl/library/luban/OnRenameListener.java diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..a79707b --- /dev/null +++ b/library/.gitignore @@ -0,0 +1,3 @@ +/build +*.iml +.DS_Sotre \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..3127278 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,134 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion COMPILE_SDK_VERSION.toInteger() + defaultConfig { + minSdkVersion MIN_SDK_VERSION.toInteger() + targetSdkVersion TARGET_SDK_VERSION.toInteger() + + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = '11' + } + + dataBinding { + enabled = true + } + viewBinding { + enabled = true + } + + sourceSets { + + main { + java.srcDirs = [ + 'src/main/java', + 'src/module_easypermission/java', + 'src/module_luban/java', + 'src/module_common/java', + + ] + + res.srcDirs = [ + 'src/main/res', + 'src/module_easypermission/res', + 'src/module_common/res', + + ] + } + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + buildToolsVersion = '30.0.3' +} + +dependencies { + def SmartRefreshLayoutVersion = "1.0.3" + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api 'androidx.cardview:cardview:1.0.0' + api 'androidx.gridlayout:gridlayout:1.0.0' + api "androidx.fragment:fragment:1.6.1" + api "androidx.fragment:fragment-ktx:1.6.1" + + api "com.squareup.retrofit2:retrofit:2.9.0" + api "com.squareup.okhttp3:okhttp:4.10.0" + api "com.squareup.okhttp3:logging-interceptor:4.10.0" + api "com.squareup.okio:okio:3.0.0" + + api "com.squareup.retrofit2:adapter-rxjava2:2.3.0" + api 'com.google.code.gson:gson:2.9.0' + +// api "com.scwang.smartrefresh:SmartRefreshLayout:${SmartRefreshLayoutVersion}" +// api "com.scwang.smartrefresh:SmartRefreshHeader:${SmartRefreshLayoutVersion}" + + + api "io.github.scwang90:refresh-layout-kernel:3.0.0-alpha" //核心必须依赖 +// api "io.github.scwang90:refresh-header-classics:3.0.0-alpha" //经典刷新头 + api "io.github.scwang90:refresh-header-material:3.0.0-alpha" //谷歌刷新头 + api "io.github.scwang90:refresh-footer-classics:3.0.0-alpha" //经典加载 + + api "io.reactivex.rxjava2:rxjava:2.2.12" + api "io.reactivex.rxjava2:rxandroid:2.1.1" + api "com.trello.rxlifecycle3:rxlifecycle:3.1.0" + api "com.trello.rxlifecycle3:rxlifecycle-android:3.1.0" + api "com.trello.rxlifecycle3:rxlifecycle-components:3.1.0" + api "com.github.bumptech.glide:glide:4.13.2" + annotationProcessor "com.github.bumptech.glide:compiler:4.13.2" + + api "com.orhanobut:logger:2.2.0" + + api "org.greenrobot:eventbus:3.3.1" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + api 'com.github.getActivity:ToastUtils:10.5' + + api 'androidx.legacy:legacy-support-v4:1.0.0' + api 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + api 'com.github.chrisbanes:PhotoView:2.3.0' + + //mmkv + api 'com.tencent:mmkv:1.2.13' + + api "jp.wasabeef:glide-transformations:3.0.1" + + //流式布局 + api 'com.google.android.flexbox:flexbox:3.0.0' + + api 'io.github.razerdp:BasePopup:3.2.1' + +// api 'com.umeng.sdk:utdid:1.1.5.3' + +// api 'com.facebook.android:facebook-android-sdk:16.2.0' +// api 'com.facebook.android:facebook-login:16.2.0' + + // 网络请求chrome数据调试 +// api 'com.facebook.stetho:stetho:1.5.1' +// api 'com.facebook.stetho:stetho-okhttp3:1.5.1' + + api project(':libs:lib_utils') + api project(':libs:lib_core') + api project(':libs:lib_encipher') + + api 'com.qcloud.cos:cos-android:5.9.25' +// api 'com.liulishuo.filedownloader:library:1.7.7' + +// api "com.github.yyued:SVGAPlayer-Android:2.6.1" +// api "com.github.yyued:SVGAPlayer-Android:latest" + implementation 'com.squareup.wire:wire-runtime:4.4.1' +} +repositories { + mavenCentral() +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/library/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 \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2bdf015 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/library/src/main/java/com/chwl/library/adapters/BaseListRecyclerViewAdapter.java b/library/src/main/java/com/chwl/library/adapters/BaseListRecyclerViewAdapter.java new file mode 100644 index 0000000..ddfad6b --- /dev/null +++ b/library/src/main/java/com/chwl/library/adapters/BaseListRecyclerViewAdapter.java @@ -0,0 +1,226 @@ +package com.chwl.library.adapters; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Created by MadisonRong on 15/7/30. + */ +public abstract class BaseListRecyclerViewAdapter + extends RecyclerView.Adapter implements List { + private final Object lock = new Object(); + private final List list; + + public BaseListRecyclerViewAdapter() { + list = new ArrayList(); + } + + public BaseListRecyclerViewAdapter(int capacity){ + list = new ArrayList(capacity); + } + + public BaseListRecyclerViewAdapter(Collection collection) { + this.list = new ArrayList(collection); + } + + @Override + public int getItemCount() { + return list.size(); + } + + @Override + public void add(int location, T object) { + synchronized (lock) { + list.add(location, object); + notifyItemInserted(location); + } + } + + @Override + public boolean add(T object) { + synchronized (lock) { + if (list.add(object)) { + int position = list.indexOf(object); + notifyItemInserted(position); + return true; + } else { + return false; + } + } + } + + @Override + public boolean addAll(Collection collection) { + synchronized (lock) { + int lastIndex = list.size(); + if (list.addAll(collection)) { + notifyItemRangeInserted(lastIndex, collection.size()); + return true; + } else { + return false; + } + } + } + + @Override + public boolean addAll(int location, Collection collection) { + synchronized (lock) { + if (list.addAll(location, collection)) { + notifyItemRangeInserted(location, collection.size()); + return true; + } else { + return false; + } + } + } + + @Override + public void clear() { + synchronized (lock) { + int size = list.size(); + list.clear(); + notifyItemRangeRemoved(0, size); + } + } + + @Override + public boolean contains(Object object) { + return list.contains(object); + } + + @Override + public boolean containsAll(Collection collection) { + return list.contains(collection); + } + + @Override + public T get(int location) { + return list.get(location); + } + + @Override + public int indexOf(Object object) { + return list.indexOf(object); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @NonNull + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public int lastIndexOf(Object object) { + return list.lastIndexOf(object); + } + + @NonNull + @Override + public ListIterator listIterator() { + return list.listIterator(); + } + + @NonNull + @Override + public ListIterator listIterator(int location) { + return list.listIterator(location); + } + + @Override + public T remove(int location) { + synchronized (lock) { + T item = list.remove(location); + notifyItemRemoved(list.indexOf(item)); + return item; + } + } + + @Override + public boolean remove(Object object) { + boolean modified = false; + synchronized (lock) { + if (list.contains(object)) { + int position = list.indexOf(object); + list.remove(position); + notifyItemRemoved(position); + modified = true; + } + } + return modified; + } + + @Override + public boolean removeAll(Collection collection) { + boolean modified = false; + synchronized (lock) { + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + Object object = iterator.next(); + if (list.contains(object)){ + int position = list.indexOf(object); + list.remove(position); + notifyItemRemoved(position); + modified = true; + } + } + } + return modified; + } + + @Override + public boolean retainAll(Collection collection) { + boolean modified = false; + synchronized (lock) { + modified = list.retainAll(collection); + } + return modified; + } + + @Override + public T set(int location, T object) { + synchronized (lock) { + T item = list.set(location, object); + notifyItemInserted(location); + return item; + } + } + + @Override + public int size() { + return list.size(); + } + + @NonNull + @Override + public List subList(int start, int end) { + return list.subList(start, end); + } + + @NonNull + @Override + public Object[] toArray() { + return list.toArray(); + } + + @NonNull + @Override + public T1[] toArray(T1[] array) { + return list.toArray(array); + } + + @Override + public boolean equals(Object o) { + return o instanceof List && list.equals(o); + } +} diff --git a/library/src/main/java/com/chwl/library/animator/AbstractPathAnimator.java b/library/src/main/java/com/chwl/library/animator/AbstractPathAnimator.java new file mode 100644 index 0000000..4d46334 --- /dev/null +++ b/library/src/main/java/com/chwl/library/animator/AbstractPathAnimator.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * 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.chwl.library.animator; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Path; +import android.view.View; +import android.view.ViewGroup; + +import com.chwl.library.R; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + + +public abstract class AbstractPathAnimator { + private final Random mRandom; + protected final Config mConfig; + + + public AbstractPathAnimator(Config config) { + mConfig = config; + mRandom = new Random(); + } + + public float randomRotation() { + return mRandom.nextFloat() * 28.6F - 14.3F; + } + + public Path createPath(AtomicInteger counter, View view, int factor) { + Random r = mRandom; + int x = r.nextInt(mConfig.xRand); + int x2 = r.nextInt(mConfig.xRand); + int y = mConfig.initY; + int y2 = counter.intValue() * 15 + mConfig.animLength * factor + r.nextInt(mConfig.animLengthRand); + factor = y2 / mConfig.bezierFactor; + x = mConfig.xPointFactor + x; + x2 = mConfig.xPointFactor + x2; + int y3 = y - y2; + y2 = y - y2 / 2; + Path p = new Path(); + p.moveTo(mConfig.initX, y); + p.cubicTo(mConfig.initX, y - factor, x, y2 + factor, x, y2); + p.moveTo(x, y2); + p.cubicTo(x, y2 - factor, x2, y3 + factor, x2, y3); + return p; + } + + public Config getmConfig() { + return mConfig; + } + + public abstract void start(View child, ViewGroup parent); + + public static class Config { + public int initX; + public int initY; + public int xRand; + public int animLengthRand; + public int bezierFactor; + public int xPointFactor; + public int animLength; + public int heartWidth; + public int heartHeight; + public int animDuration; + + public static Config fromTypeArray(TypedArray typedArray) { + Config config = new Config(); + Resources res = typedArray.getResources(); + config.initX = (int) typedArray.getDimension(R.styleable.HeartLayout_initX, + res.getDimensionPixelOffset(R.dimen.heart_anim_init_x)); + config.initY = (int) typedArray.getDimension(R.styleable.HeartLayout_initY, + res.getDimensionPixelOffset(R.dimen.heart_anim_init_y)); + config.xRand = (int) typedArray.getDimension(R.styleable.HeartLayout_xRand, + res.getDimensionPixelOffset(R.dimen.heart_anim_bezier_x_rand)); + config.animLength = (int) typedArray.getDimension(R.styleable.HeartLayout_animLength, + res.getDimensionPixelOffset(R.dimen.heart_anim_length)); + config.animLengthRand = (int) typedArray.getDimension(R.styleable.HeartLayout_animLengthRand, + res.getDimensionPixelOffset(R.dimen.heart_anim_length_rand)); + config.bezierFactor = typedArray.getInteger(R.styleable.HeartLayout_bezierFactor, + res.getInteger(R.integer.heart_anim_bezier_factor)); + config.xPointFactor = (int) typedArray.getDimension(R.styleable.HeartLayout_xPointFactor, + res.getDimensionPixelOffset(R.dimen.heart_anim_x_point_factor)); + config.heartWidth = (int) typedArray.getDimension(R.styleable.HeartLayout_heart_width, + res.getDimensionPixelOffset(R.dimen.heart_size_width)); + config.heartHeight = (int) typedArray.getDimension(R.styleable.HeartLayout_heart_height, + res.getDimensionPixelOffset(R.dimen.heart_size_height)); + config.animDuration = typedArray.getInteger(R.styleable.HeartLayout_anim_duration, + res.getInteger(R.integer.anim_duration)); + return config; + } + + + } + + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/animator/PathAnimator.java b/library/src/main/java/com/chwl/library/animator/PathAnimator.java new file mode 100644 index 0000000..22d0465 --- /dev/null +++ b/library/src/main/java/com/chwl/library/animator/PathAnimator.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * 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.chwl.library.animator; + +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PathAnimator extends AbstractPathAnimator { + private final AtomicInteger mCounter = new AtomicInteger(0); + private Handler mHandler; + + public PathAnimator(Config config) { + super(config); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public void start(final View child, final ViewGroup parent) { + parent.addView(child); + FloatAnimation anim = new FloatAnimation(createPath(mCounter, parent, 2), randomRotation(), parent, child); + anim.setDuration(mConfig.animDuration); + anim.setInterpolator(new LinearInterpolator()); + anim.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + mHandler.post(new Runnable() { + @Override + public void run() { + parent.removeView(child); + } + }); + mCounter.decrementAndGet(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + + @Override + public void onAnimationStart(Animation animation) { + mCounter.incrementAndGet(); + } + }); + anim.setInterpolator(new LinearInterpolator()); + child.startAnimation(anim); + } + + static class FloatAnimation extends Animation { + private PathMeasure mPm; + private View mView; + private float mDistance; + private float mRotation; + + public FloatAnimation(Path path, float rotation, View parent, View child) { + mPm = new PathMeasure(path, false); + mDistance = mPm.getLength(); + mView = child; + mRotation = rotation; + parent.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + @Override + protected void applyTransformation(float factor, Transformation transformation) { + Matrix matrix = transformation.getMatrix(); + mPm.getMatrix(mDistance * factor, matrix, PathMeasure.POSITION_MATRIX_FLAG); + mView.setRotation(mRotation * factor); + float scale = 1F; + if (3000.0F * factor < 200.0F) { + scale = scale(factor, 0.0D, 0.06666667014360428D, 0.20000000298023224D, 1.100000023841858D); + } else if (3000.0F * factor < 300.0F) { + scale = scale(factor, 0.06666667014360428D, 0.10000000149011612D, 1.100000023841858D, 1.0D); + } + mView.setScaleX(scale); + mView.setScaleY(scale); + transformation.setAlpha(1.0F - factor); + } + } + + private static float scale(double a, double b, double c, double d, double e) { + return (float) ((a - b) / (c - b) * (e - d) + d); + } +} diff --git a/library/src/main/java/com/chwl/library/annatation/ActLayoutRes.java b/library/src/main/java/com/chwl/library/annatation/ActLayoutRes.java new file mode 100644 index 0000000..217594b --- /dev/null +++ b/library/src/main/java/com/chwl/library/annatation/ActLayoutRes.java @@ -0,0 +1,15 @@ +package com.chwl.library.annatation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Created by huangmeng1 on 2018/5/7. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ActLayoutRes { + int value(); +} diff --git a/library/src/main/java/com/chwl/library/base/IMvpBaseView.java b/library/src/main/java/com/chwl/library/base/IMvpBaseView.java new file mode 100644 index 0000000..a2828b0 --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/IMvpBaseView.java @@ -0,0 +1,11 @@ +package com.chwl.library.base; + +/** + *

MVP 模式View 基类

+ * + * @author jiahui + * @date 2017/12/7 + */ +public interface IMvpBaseView { + +} diff --git a/library/src/main/java/com/chwl/library/base/PresenterEvent.java b/library/src/main/java/com/chwl/library/base/PresenterEvent.java new file mode 100644 index 0000000..f29b41c --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/PresenterEvent.java @@ -0,0 +1,12 @@ +package com.chwl.library.base; + +public enum PresenterEvent { + + CREATE, + START, + RESUME, + PAUSE, + STOP, + DESTROY + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/base/factory/AbstractMvpPresenter.java b/library/src/main/java/com/chwl/library/base/factory/AbstractMvpPresenter.java new file mode 100644 index 0000000..f192692 --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/AbstractMvpPresenter.java @@ -0,0 +1,145 @@ +package com.chwl.library.base.factory; + +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.trello.rxlifecycle3.LifecycleProvider; +import com.trello.rxlifecycle3.LifecycleTransformer; +import com.trello.rxlifecycle3.OutsideLifecycleException; +import com.trello.rxlifecycle3.RxLifecycle; +import com.chwl.library.base.IMvpBaseView; +import com.chwl.library.base.PresenterEvent; + +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; + +/** + *

MVP模式Present 基类 ,跟Activity,Fragment等生命周期绑定

+ * + * @author jiahui + * @date 2017/12/7 + */ +public abstract class AbstractMvpPresenter implements LifecycleProvider { + private static final String TAG = "Super-mvp"; + private final BehaviorSubject lifecycleSubject = BehaviorSubject.create(); + protected V mMvpView; + + /** 绑定View */ + public void attachMvpView(V mvpView) { + + this.mMvpView = mvpView; + logInfo("Presenter attachMvpView..."); + } + + /** 解除绑定的View */ + public void detachMvpView() { + mMvpView = null; + logInfo("Presenter detachMvpView..."); + } + + /** + * 获取V层的接口View + * + * @return 当前的接口View + */ + public V getMvpView() { + return mMvpView; + } + + /** + * Presenter 被创建后调用 + * + * @param saveState 被意外销毁后的Bundle + */ + public void onCreatePresenter(@Nullable Bundle saveState) { + logInfo("Presenter onCreatePresenter..."); + lifecycleSubject.onNext(PresenterEvent.CREATE); + } + + public void onStartPresenter() { + logInfo("Presenter onStartPresenter..."); + lifecycleSubject.onNext(PresenterEvent.START); + } + + public void onResumePresenter() { + logInfo("Presenter onResumePresenter..."); + lifecycleSubject.onNext(PresenterEvent.RESUME); + } + + public void onPausePresenter() { + logInfo("Presenter onPausePresenter..."); + lifecycleSubject.onNext(PresenterEvent.PAUSE); + } + + public void onStopPresenter() { + logInfo("Presenter onStopPresenter..."); + lifecycleSubject.onNext(PresenterEvent.STOP); + } + + /** Presenter被销毁的时候调用,可以在此释放资源等 */ + public void onDestroyPresenter() { + logInfo("Presenter onDestroyPresenter..."); + lifecycleSubject.onNext(PresenterEvent.DESTROY); + } + + /** + * 在Presenter被意外销毁时调用,它的调用时机和Activity,Fragment,View中的onSaveInstanceState()方法调用时机相同 + * + * @param outState 保存消息的Bundle + */ + public void onSaveInstanceState(Bundle outState) { + logInfo("Presenter onSaveInstanceState..."); + } + + @Override + @NonNull + public Observable lifecycle() { + return lifecycleSubject.hide(); + } + + @Override + @NonNull + public LifecycleTransformer bindUntilEvent(@NonNull PresenterEvent event) { + return RxLifecycle.bindUntilEvent(lifecycleSubject, event); + } + + @Override + @NonNull + public LifecycleTransformer bindToLifecycle() { + return RxLifecycle.bind(lifecycleSubject, PRESENTER_LIFECYCLE); +// return RxLifecycleAndroid.bindActivity(lifecycleSubject); + } + + private final Function PRESENTER_LIFECYCLE = + new Function() { + @Override + public PresenterEvent apply(PresenterEvent lastEvent) throws Exception { + switch (lastEvent) { + case CREATE: + return PresenterEvent.DESTROY; + case START: + return PresenterEvent.STOP; + case RESUME: + return PresenterEvent.PAUSE; + case PAUSE: + return PresenterEvent.STOP; + case STOP: + return PresenterEvent.DESTROY; + case DESTROY: + throw new OutsideLifecycleException("Cannot bind to Presenter lifecycle when outside of it."); + default: + throw new UnsupportedOperationException("Binding to " + lastEvent + " not yet implemented"); + } + } + }; + + private void logInfo(String msg) { + if (false) + Log.e(TAG, msg); + } + +} diff --git a/library/src/main/java/com/chwl/library/base/factory/BaseMvpProxy.java b/library/src/main/java/com/chwl/library/base/factory/BaseMvpProxy.java new file mode 100644 index 0000000..87df05d --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/BaseMvpProxy.java @@ -0,0 +1,144 @@ +package com.chwl.library.base.factory; + +import android.os.Bundle; +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.base.IMvpBaseView; +import com.chwl.library.utils.ResUtil; + + +/** + *

管理Presenter的声明周期以及与View之间的关联

+ * + * @author jiahui + * @date 2017/12/8 + */ +public class BaseMvpProxy> + implements PresenterProxyInterface { + private static final String TAG = "Super-mvp"; + private static final String KEY_PRESENTER = "key_presenter"; + + private PresenterMvpFactory mMvpFactory; + private P mPresenter; + private Bundle mBundle; + private boolean mIsAttachView; + + public BaseMvpProxy(PresenterMvpFactory mvpFactory) { + this.mMvpFactory = mvpFactory; + } + + + @Override + public void setPresenterFactory(PresenterMvpFactory presenterFactory) { + if (mPresenter != null) { + throw new IllegalArgumentException(ResUtil.getString(R.string.base_factory_basemvpproxy_01)); + } + this.mMvpFactory = presenterFactory; + } + + @Override + public PresenterMvpFactory getPresenterFactory() { + return mMvpFactory; + } + + @Override + public P getMvpPresenter() { + logInfo("Proxy getMvpPresenter..."); + //如果之前创建过且是意外销毁则从Bundle中恢复 + if (mMvpFactory != null) { + if (mPresenter == null) { + mPresenter = mMvpFactory.createMvpPresenter(); + mPresenter.onCreatePresenter(mBundle == null ? null : mBundle.getBundle(KEY_PRESENTER)); + } + } + logInfo("Proxy getMvpPresenter..." + mPresenter); + return mPresenter; + } + + /** 销毁Presenter持有的View */ + private void onDetachMvpView() { + logInfo("Proxy onDetachMvpView..."); + if (mPresenter != null && mIsAttachView) { + mPresenter.detachMvpView(); + mIsAttachView = false; + } + } + + public void onStart() { + logInfo("Proxy onStart..."); + if (mPresenter != null) + mPresenter.onStartPresenter(); + } + + /** + * 绑定Presenter与View + * + * @param mvpView 当前view接口类型 + */ + public void onResume(V mvpView) { + getMvpPresenter(); + logInfo("Proxy onResume..."); + if (mPresenter != null && !mIsAttachView) { + mPresenter.attachMvpView(mvpView); + mIsAttachView = true; + mPresenter.onResumePresenter(); + } + } + + public void onPause() { + logInfo("Proxy onPause..."); + if (mPresenter != null) + mPresenter.onPausePresenter(); + } + + public void onStop() { + logInfo("Proxy onStop..."); + if (mPresenter != null) + mPresenter.onStopPresenter(); + } + + + /** 销毁Presenter */ + public void onDestroy() { + logInfo("Proxy onDestroy..."); + if (mPresenter != null) { + onDetachMvpView(); + mPresenter.onDestroyPresenter(); + mPresenter = null; + } + } + + /** + * 意外销毁的时候调用 + * + * @return Bundle ,存入回调给Presenter的Bundle和当前Presenter的id,在调用方(activity)中保存 + */ + public Bundle onSaveInstanceState() { + logInfo("Proxy onSaveInstanceState..."); + Bundle bundle = new Bundle(); + getMvpPresenter(); + if (mPresenter != null) { + Bundle presenterBundle = new Bundle(); + //回到Presenter + mPresenter.onSaveInstanceState(presenterBundle); + //保存presenterBundle + bundle.putBundle(KEY_PRESENTER, presenterBundle); + } + return bundle; + } + + /*** + * 意外关闭Presenter的时候恢复Presenter + * @param saveInstanceState 意外关闭Presenter时存储的Bundle + */ + public void onRestoreInstanceState(Bundle saveInstanceState) { + logInfo("Proxy onRestoreInstanceState... Presenter=" + mPresenter); + mBundle = saveInstanceState; + } + + private void logInfo(String msg) { + if (false) + Log.e(TAG, msg); + } +} diff --git a/library/src/main/java/com/chwl/library/base/factory/CreatePresenter.java b/library/src/main/java/com/chwl/library/base/factory/CreatePresenter.java new file mode 100644 index 0000000..c59acd3 --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/CreatePresenter.java @@ -0,0 +1,18 @@ +package com.chwl.library.base.factory; + + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + *

标注创建Presenter的注解

+ * + * @author jiahui + * @date 2017/12/8 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface CreatePresenter { + Class value(); +} diff --git a/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactory.java b/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactory.java new file mode 100644 index 0000000..6acae82 --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactory.java @@ -0,0 +1,19 @@ +package com.chwl.library.base.factory; + + +import com.chwl.library.base.IMvpBaseView; + +/** + *

Presenter 工厂接口

+ * + * @author jiahui + * @date 2017/12/7 + */ +public interface PresenterMvpFactory> { + /** + * 创建Presenter方法 + * + * @return 创建的Presenter + */ + P createMvpPresenter(); +} diff --git a/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactoryImpl.java b/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactoryImpl.java new file mode 100644 index 0000000..7cb789d --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/PresenterMvpFactoryImpl.java @@ -0,0 +1,49 @@ +package com.chwl.library.base.factory; + +import com.chwl.library.R; +import com.chwl.library.base.IMvpBaseView; +import com.chwl.library.utils.ResUtil; + +/** + *

Presenter 工厂实现类

+ * + * @author jiahui + * @date 2017/12/8 + */ +public class PresenterMvpFactoryImpl> + implements PresenterMvpFactory { + private final Class

mPresenterClass; + + private PresenterMvpFactoryImpl(Class

presenterClass) { + this.mPresenterClass = presenterClass; + } + + /** + * 根据注解创建Presenter的工厂实现方法 + * + * @param viewClass 需要创建Presenter的V层实现类 + * @param 当前View的实现接口类型 + * @param

当前要创建的Presenter类型 + * @return 工厂实现类 + */ + public static > PresenterMvpFactoryImpl + createFactory(Class viewClass) { + CreatePresenter annotation = viewClass.getAnnotation(CreatePresenter.class); + Class

pClass = null; + if (annotation != null) { + pClass = (Class

) annotation.value(); + } + return pClass == null ? null : new PresenterMvpFactoryImpl<>(pClass); + + } + + @Override + public P createMvpPresenter() { + try { + return mPresenterClass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(ResUtil.getString(R.string.base_factory_presentermvpfactoryimpl_01), e); + } + } +} diff --git a/library/src/main/java/com/chwl/library/base/factory/PresenterProxyInterface.java b/library/src/main/java/com/chwl/library/base/factory/PresenterProxyInterface.java new file mode 100644 index 0000000..cb1a988 --- /dev/null +++ b/library/src/main/java/com/chwl/library/base/factory/PresenterProxyInterface.java @@ -0,0 +1,34 @@ +package com.chwl.library.base.factory; + + +import com.chwl.library.base.IMvpBaseView; + +/** + *

提供设置工厂,获取工厂,获取Presenter的方法,由V层实现这个接口

+ * + * @author jiahui + * @date 2017/12/8 + */ +public interface PresenterProxyInterface> { + + /** + * 设置Presenter的工厂类,这个方法只能在getMvpPresenter()之前调用,如果Presenter已经创建了则不能再更改 + * + * @param presenterFactory 要设置的Presenter的工厂类型 + */ + void setPresenterFactory(PresenterMvpFactory presenterFactory); + + /** + * 获取Presenter的工厂类 + * + * @return 返回的是PresenterMvpFactory类型实例 + */ + PresenterMvpFactory getPresenterFactory(); + + /** + * 获取创建的Presenter + * + * @return 指定的目标Presenter + */ + P getMvpPresenter(); +} diff --git a/library/src/main/java/com/chwl/library/bindinglist/BaseItem.java b/library/src/main/java/com/chwl/library/bindinglist/BaseItem.java new file mode 100644 index 0000000..b9ec688 --- /dev/null +++ b/library/src/main/java/com/chwl/library/bindinglist/BaseItem.java @@ -0,0 +1,35 @@ +package com.chwl.library.bindinglist; + +import android.content.Context; + +/** + * Created by lvzebiao on 2018/10/25. + */ + +public abstract class BaseItem implements IItem { + + protected Context context; + + public T data; + + protected int spanSize; + + public BaseItem(Context context, T data) { + this(context, data, 1); + } + + public BaseItem(Context context, T data, int spanSize) { + this.context = context; + this.data = data; + this.spanSize = spanSize; + } + + @Override + public int getSpanSize() { + return 1; + } + + public T data() { + return data; + } +} diff --git a/library/src/main/java/com/chwl/library/bindinglist/IItem.java b/library/src/main/java/com/chwl/library/bindinglist/IItem.java new file mode 100644 index 0000000..61345e7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/bindinglist/IItem.java @@ -0,0 +1,13 @@ +package com.chwl.library.bindinglist; + +/** + * Created by lvzebiao on 2018/10/23. + */ +public interface IItem { + /** + * 格子布局时需要用到,默认返回1,线性布局忽略即可 + */ + int getSpanSize(); + + int getType(); +} diff --git a/library/src/main/java/com/chwl/library/bindinglist/ItemViewHolder.java b/library/src/main/java/com/chwl/library/bindinglist/ItemViewHolder.java new file mode 100644 index 0000000..dc36579 --- /dev/null +++ b/library/src/main/java/com/chwl/library/bindinglist/ItemViewHolder.java @@ -0,0 +1,34 @@ +package com.chwl.library.bindinglist; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by lvzebiao on 2018/10/23. + */ + +public class ItemViewHolder extends RecyclerView.ViewHolder { + + protected final ViewDataBinding binding; + + public ItemViewHolder(ViewDataBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bindTo(int variableId, IItem item) { + binding.setVariable(variableId, item); + binding.executePendingBindings(); + } + + public static ItemViewHolder create(ViewGroup parent, int viewType) { + ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), + viewType, parent, false); + return new ItemViewHolder(binding); + } + +} diff --git a/library/src/main/java/com/chwl/library/bindinglist/MultiTypeAdapter.java b/library/src/main/java/com/chwl/library/bindinglist/MultiTypeAdapter.java new file mode 100644 index 0000000..72942e8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/bindinglist/MultiTypeAdapter.java @@ -0,0 +1,353 @@ +package com.chwl.library.bindinglist; + +import android.os.Looper; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableArrayList; +import androidx.databinding.ObservableList; +import androidx.recyclerview.widget.RecyclerView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by lvzebiao on 2018/10/23. + */ + +public class MultiTypeAdapter extends RecyclerView.Adapter { + + private final WeakReferenceOnListChangedCallback callback = new WeakReferenceOnListChangedCallback<>(this); + private List headersItems = new ArrayList<>(); + private ObservableList footersItems = new ObservableArrayList<>(); + /** + * 此数据源表示真正的数据, 保留用于其他逻辑计算 + */ + private List dataItems; + /** + * 此list表示RecyclerView显示的所有item,即header+data+footer + */ + private ObservableList allItems = new ObservableArrayList<>(); + private boolean canLoadMore = false; + private RecyclerView recyclerView; + private int variableId; + private boolean needItemClick; + private T loadMoreItem; + private List statusList = new ArrayList<>(); + private T startLoadingItem; + private T netErrorItem; + private T emptyItem; + private OnItemClickListener listener; + + public MultiTypeAdapter(List list, int variableId, boolean needItemClick) { + if (list == null) { + list = new ObservableArrayList<>(); + } + this.dataItems = list; + this.allItems.addAll(this.dataItems); + this.variableId = variableId; + this.needItemClick = needItemClick; + } + + public MultiTypeAdapter(int variableId, boolean needItemClick) { + this(null, variableId, needItemClick); + } + + public MultiTypeAdapter(int variableId) { + this(null, variableId, false); + } + + static void ensureChangeOnMainThread() { + if (Thread.currentThread() != Looper.getMainLooper().getThread()) { + throw new IllegalStateException("You must only modify the ObservableList on the main thread."); + } + } + + public List getHeadersItems() { + return headersItems; + } + + public List getData() { + return dataItems; + } + + public List getAllItems() { + return allItems; + } + + public void setCanLoadMore(boolean canLoadMore) { + this.canLoadMore = canLoadMore; + } + + /** + * 这方法不会清除header,清除header调用 + * {@link #clearAllItem()} + */ + public void clearData() { + //清除数据源,也要清除底部加载更多 + dataItems.clear(); + statusList.clear(); + footersItems.clear(); + hideStatus(); + if (headersItems.size() == 0) { + allItems.clear(); + } else { +// List tempList = new ArrayList<>(); +// for (int i = headersItems.size(); i < allItems.size(); i++) { +// tempList.add(allItems.get(i)); +// } +// allItems.removeAll(tempList); + allItems.clear(); + allItems.addAll(headersItems); + } + } + + /** + * 清除所有列表,这个方法比较少调用 + * 一般header不会清除,只清除data部分调用 + * {@link #clearData()} + */ + public void clearAllItem() { + dataItems.clear(); + statusList.clear(); + footersItems.clear(); + headersItems.clear(); + allItems.clear(); + } + + public void addData(List list) { + dataItems.addAll(list); + int insertPos = allItems.size() - footersItems.size(); + if (insertPos < 0) { + insertPos = 0; + } + allItems.addAll(insertPos, list); + } + + public void addHeaderItem(T headerItem) { + headersItems.add(headerItem); + allItems.add(0, headerItem); + } + + public void setLoadMoreView(T item) { + if (!canLoadMore || item == null) { + return; + } + if (footersItems.size() == 0) { + if (loadMoreItem == null) { + loadMoreItem = item; + } + footersItems.add(loadMoreItem); + allItems.add(loadMoreItem); + } + } + + public void removeLoadMoreView() { + if (!canLoadMore || loadMoreItem == null) { + return; + } + if (footersItems.size() == 1) { + footersItems.clear(); + if (allItems.contains(loadMoreItem)) { + allItems.remove(loadMoreItem); + } + } + } + + /** + * 此方法并不是所有list都需要,不需要则传null + * 只是客户端有个加载动画,所以添加 + */ + public void startLoading(T item) { + if (item == null) { + return; + } + if (statusList.size() == 0) { + if (startLoadingItem == null) { + startLoadingItem = item; + } + statusList.add(startLoadingItem); + allItems.add(startLoadingItem); + } + } + + /** + * 如果有数据不添加错误item + */ + public void setNetError(T item) { + if (item == null) { + return; + } + if (statusList.size() == 0) { + if (netErrorItem == null) { + netErrorItem = item; + } + statusList.add(netErrorItem); + allItems.add(netErrorItem); + } else { + if (emptyItem != null && allItems.contains(emptyItem)) { + allItems.remove(emptyItem); + statusList.clear(); + setNetError(item); + } + } + } + + public void setEmpty(T item) { + if (item == null) { + return; + } + if (statusList.size() == 0) { + if (emptyItem == null) { + emptyItem = item; + } + statusList.add(emptyItem); + allItems.add(emptyItem); + } else { + //说明状态列表有item,可能是error,也可能就是empty + //1.如果是empty则不需要操作,此操作相当于空列表,再刷新还是空列表,error同理 + //2.如果是error,先移除,再添加empty + if (netErrorItem != null && allItems.contains(netErrorItem)) { + allItems.remove(netErrorItem); + statusList.clear(); + setEmpty(item); + } + } + } + + public void hideStatus() { + statusList.clear(); + if (startLoadingItem != null) { + allItems.remove(startLoadingItem); + } + + if (netErrorItem != null) { + allItems.remove(netErrorItem); + } + if (emptyItem != null) { + allItems.remove(emptyItem); + } + } + + public void clearLoarMoreView() { + footersItems.clear(); + } + + @Override + public int getItemViewType(int position) { + return allItems.get(position).getType(); + } + + @NonNull + @Override + public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return ItemViewHolder.create(parent, viewType); + } + + @Override + public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { + final T item = getIndexItem(position); + holder.bindTo(variableId, item); + if (needItemClick) { + holder.binding.getRoot().setOnClickListener(v -> { + if (listener != null) { + listener.onClick(item); + } + }); + } + } + + @Override + public int getItemCount() { + return allItems.size(); + } + + public T getIndexItem(int position) { + return allItems.get(position); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + if (this.recyclerView == null && allItems != null) { + allItems.addOnListChangedCallback(callback); + } + this.recyclerView = recyclerView; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + this.listener = listener; + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + if (this.recyclerView != null && allItems != null) { + allItems.removeOnListChangedCallback(callback); + } + this.recyclerView = null; + } + + public interface OnItemClickListener { + void onClick(T item); + } + + private static class WeakReferenceOnListChangedCallback extends ObservableList.OnListChangedCallback> { + final WeakReference> adapterRef; + + WeakReferenceOnListChangedCallback(MultiTypeAdapter adapter) { + this.adapterRef = new WeakReference<>(adapter); + } + + @Override + public void onChanged(ObservableList sender) { + MultiTypeAdapter adapter = adapterRef.get(); + if (adapter == null) { + return; + } + ensureChangeOnMainThread(); + adapter.notifyDataSetChanged(); + } + + @Override + public void onItemRangeChanged(ObservableList sender, final int positionStart, final int itemCount) { + MultiTypeAdapter adapter = adapterRef.get(); + if (adapter == null) { + return; + } + ensureChangeOnMainThread(); + adapter.notifyItemRangeChanged(positionStart, itemCount); + } + + @Override + public void onItemRangeInserted(ObservableList sender, final int positionStart, final int itemCount) { + MultiTypeAdapter adapter = adapterRef.get(); + if (adapter == null) { + return; + } + ensureChangeOnMainThread(); + adapter.notifyItemRangeInserted(positionStart, itemCount); + } + + @Override + public void onItemRangeMoved(ObservableList sender, final int fromPosition, final int toPosition, final int itemCount) { + MultiTypeAdapter adapter = adapterRef.get(); + if (adapter == null) { + return; + } + ensureChangeOnMainThread(); + for (int i = 0; i < itemCount; i++) { + adapter.notifyItemMoved(fromPosition + i, toPosition + i); + } + } + + @Override + public void onItemRangeRemoved(ObservableList sender, final int positionStart, final int itemCount) { + MultiTypeAdapter adapter = adapterRef.get(); + if (adapter == null) { + return; + } + ensureChangeOnMainThread(); + adapter.notifyItemRangeRemoved(positionStart, itemCount); + } + } +} diff --git a/library/src/main/java/com/chwl/library/constants/ConstantsLib.java b/library/src/main/java/com/chwl/library/constants/ConstantsLib.java new file mode 100644 index 0000000..11d9649 --- /dev/null +++ b/library/src/main/java/com/chwl/library/constants/ConstantsLib.java @@ -0,0 +1,7 @@ +package com.chwl.library.constants; + +public @interface ConstantsLib { + public @interface Key{ + String Permissions_Img = "Permissions_Img"; + } +} diff --git a/library/src/main/java/com/chwl/library/coremanager/AbstractBaseCore.java b/library/src/main/java/com/chwl/library/coremanager/AbstractBaseCore.java new file mode 100644 index 0000000..fc1e844 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/AbstractBaseCore.java @@ -0,0 +1,36 @@ +/** + * 每个core实现类都应该继承此类 + * 提供一些基础设施给子类使用 + */ +package com.chwl.library.coremanager; + +import android.content.Context; + +import com.chwl.library.utils.config.BasicConfig; + + +/** + * @author daixiang + * + */ +public abstract class AbstractBaseCore implements IBaseCore { + + public AbstractBaseCore() { + // 确保有默认构造函数 + } + + protected Context getContext() { + return BasicConfig.INSTANCE.getAppContext(); + } + + + protected void notifyClients(Class clientClass, String methodName, Object... args) { + CoreManager.notifyClients(clientClass, methodName, args); + } + + protected void notifyClients(Class clientClass, String methodName) { + CoreManager.notifyClients(clientClass, methodName); + } + + +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreError.java b/library/src/main/java/com/chwl/library/coremanager/CoreError.java new file mode 100644 index 0000000..6034f14 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreError.java @@ -0,0 +1,257 @@ +/** + * + */ +package com.chwl.library.coremanager; + +/** + * @author daixiang + */ +public class CoreError { + + // error code 定义 + public static final int NETWORK_ERROR = 1000; + public static final int DB_ERROR = 1001; + public static final int TIMEOUT_ERROR = 1002; + public static final int SERVER_ERROR = 1003; + public static final int UNKNOWN_ERROR = 1004; + public static final int TRAFFIC_FORBIDDEN_ERROR = 1005; + public static final int ILLEGAL_ACCESS_ERROR = 1006;//非法访问 + public static final int ILLEGAL_SMS_CODE_ERROR = 1007;//非法短信验证码 + public static final int DUPLICATE_ERROR = 1008; // 存在重复的记录 + public static final int NEWTORK_ERROR_ONLY_WIFI = 1009; // 仅wifi下可以访问网络 + public static final int SERVER_ERROR_USER_FORBIDDEN = 1010; // 黑名单用户 + public static final int SERVER_ERROR_ILLEGAL_CONTENT = 1011; // 内容不合法 + public static final int DATA_NOT_FOUND = 1404; // 数据找不到 + + //JX 错误吗定义 + public static final int ACCESS_DENIED = 100 ;//拒绝访问 + + public static final int INVALID_REQUEST = 101;//请求不合法 + + public static final int INVALID_REQUEST_SCHEME = 102;//错误的请求协议 + + public static final int INVALID_REQUEST_METHOD = 103;//错误的请求方法 + + public static final int INVALID_CLIENT_ID = 104; //client id不存在或已删除 + + public static final int CLIENT_ID_IS_BLOCKED = 105;//client id已被禁用 + + public static final int UNAUTHORIZED_CLIENT_ID = 106; //client id未授权 + + public static final int USERNAME_PASSWORD_MISMATCH = 107; //用户名密码不匹配 + + public static final int INVALID_REQUEST_SCOPE = 108; //访问的scope不合法,开发者不用太关注,一般不会出现该错误 + + public static final int INVALID_USER = 109; //用户不存在或已删除 + + public static final int USER_HAS_BLOCKED = 110; //用户已被屏蔽 + + public static final int INVALID_TOKEN = 111;//token不存在或已被用户删除,或者用户修改了密码 + + public static final int ACCESS_TOKEN_IS_MISSING = 112; //未找到access_token + + public static final int ACCESS_TOKEN_HAS_EXPIRED = 113;//access_token已过期 + + public static final int INVALID_REQUEST_URI = 114; //请求地址未注册 + + public static final int INVALID_CREDENTIAL_1 = 115; //用户未授权访问此数据 + + public static final int INVALID_CREDENTIAL_2 = 116; //client id未申请此权限 + + public static final int NOT_TRIAL_USER = 117; //未注册的测试用户 + + public static final int REQUIRED_PARAMETER_IS_MISSING = 118; //缺少参数 + + public static final int INVALID_GRANT = 119;//invalid grant type" + + public static final int UNSUPPORTED_GRANT_TYPE = 120;//错误的grant_type + + public static final int UNSUPPORTED_RESPONSE_TYPE = 121; //错误的response_type + + public static final int CLIENT_SECRET_MISMATCH = 122; //client_secret不匹配 + + public static final int REDIRECT_URI_MISMATCH = 123; //redirect_uri不匹配 + + public static final int INVALID_AUTHORIZATION_CODE = 124;//authorization_code不存在或已过期 + + public static final int ACCESS_TOKEN_HAS_EXPIRED_SINCE_PASSWORD_CHANGED = 125;//因用户修改密码而导致access_token过期 + + public static final int ACCESS_TOKEN_HAS_NOT_EXPIRED = 126; //access_token未过期; + + public static final int UNSUPPORTED_TICKET_ISSUE_TYPE = 127;//unsupported ticket issue type" + + public static final int INVALID_TICKET = 128;//ticket不存在或已过期 + + public static final int TICKET_IS_MISSING = 129; //未找到ticket + + public static final int TICKET_HAS_EXPIRED = 130; //ticket过期 + + public static final int TICKET_HAS_NOT_EXPIRED = 131; //ticket未过期 + + public static final int TICKET_HAS_EXPIRED_SINCE_PASSWORD_CHANGED = 132; //因为用户修改密码而ticket过期 + + public static final int INVALID_SCOPE = 133; + + public static final int RATE_LIMIT_EXCEEDED1 = 134;//用户访问速度限制 + + public static final int RATE_LIMIT_EXCEEDED2 = 135;//IP访问速度限制 + + public static final int INVALID_IDENTIFYING_CODE = 150; //不可用的验证码 + + public static final int INVALID_USERNAME = 151; //用户名不合法 + + public static final int USER_HAS_SIGNED_UP = 152;//用户名已被注册 + + public static final int INVALID_RESET_CODE = 153;//重置码无效 + + public static final int INVALID_NICK = 161; //昵称不合法 + + public static final int INVALID_THIRD_TOKEN = 162; //第三方token不合法 + + public static final int THIRD_ACCOUNT_HAVE_BIND = 163; //第三方账户已经绑定或之前已使用该账户登陆过系统 + + public static final int UNBIND_OPENID_NOT_MATCH = 164; //账户解绑失败 + + public static final int UNBIND_MAIN_ACCOUNT = 165;//解绑主账户错误 + + public static final int SUCCESS = 200; //成功 + + public static final int INVALID_SERVICE = 199;//服务不可用 + + public static final int UNKNOWN = 999;//未知错误 + + + // 登录错误码 + public static final int AUTH_USER_NOT_EXIST = 2000; + public static final int AUTH_PASSWORD_ERROR = 2001; + public static final int AUTH_USER_BANNED = 2002; + public static final int NO_LOGIN_ERROR = 2003;//没有登录 + public static final int NEED_RELOGIN_ERROR = 2004;//需要重新登录 + public static final int LOGIN_EXPIRED_ERROR = 2005;//登录信息过期 + public static final int REGISTER_ERROR = 2006;//注册失败 + public static final int THIRD_PARTY_LOGIN_ERROR = 2007;//第三方登录失败 + public static final int THIRD_PARTY_BIND_TOKEN_INVALID = 2008;//第三方绑定失败, token 无效 + public static final int THIRD_PARTY_BIND_ACCOUNT_HAVE_BIND = 2009;//第三方绑定失败, 第三方账户已经绑定或之前已使用该账户登陆过系统 + public static final int THIRD_PARTY_UNBIND_ERROR = 2010;//第三方解绑失败 + + // 文件错误码 + public static final int FILE_ERROR = 3000;//文件通用错误 + public static final int FILE_NO_ENOUGH_SPACE = 3001;//磁盘剩余空间不够 + + public int code; + public String message; + public Throwable throwable; + + public static CoreError empty() { + return new CoreError(-1, null); + } + + public CoreError(int code, String message) { + this.message = message; + this.code = code; + } + + public CoreError(int code) { + this.code = code; + } + + public CoreError(int code, String message, Throwable throwable) { + this.code = code; + this.message = message; + this.throwable = throwable; + } + + public static CoreError serverError() { + CoreError error = empty(); + error.code = SERVER_ERROR; + return error; + } + + public static CoreError timeoutError() { + CoreError error = empty(); + error.code = TIMEOUT_ERROR; + return error; + } + + public static CoreError networkError() { + CoreError error = empty(); + error.code = NETWORK_ERROR; + return error; + } + + public static CoreError dbError() { + CoreError error = empty(); + error.code = DB_ERROR; + return error; + } + + public static CoreError unkonwnError() { + CoreError error = empty(); + error.code = UNKNOWN_ERROR; + return error; + } + + public static CoreError noLoginError() { + CoreError error = empty(); + error.code = NO_LOGIN_ERROR; + return error; + } + + public static CoreError needReloginError() { + CoreError error = empty(); + error.code = NEED_RELOGIN_ERROR; + return error; + } + + public static CoreError duplicateError() { + CoreError error = empty(); + error.code = DUPLICATE_ERROR; + return error; + } + + public static CoreError onlyWifiAccessError() { + CoreError error = empty(); + error.code = NEWTORK_ERROR_ONLY_WIFI; + return error; + } + + public static CoreError userForbiddenError() { + CoreError error = empty(); + error.code = SERVER_ERROR_USER_FORBIDDEN; + return error; + } + + public static CoreError illegalContentError() { + CoreError error = empty(); + error.code = SERVER_ERROR_ILLEGAL_CONTENT; + return error; + } + + public static CoreError thirdpartyLoginError() { + CoreError error = empty(); + error.code = THIRD_PARTY_LOGIN_ERROR; + return error; + } + + public static CoreError thirdpartyBindError() { + CoreError error = empty(); + error.code = THIRD_PARTY_BIND_TOKEN_INVALID; + return error; + } + + public static CoreError thirdpartyUnbindError() { + CoreError error = empty(); + error.code = THIRD_PARTY_UNBIND_ERROR; + return error; + } + + public static CoreError dataNotFoundError() { + CoreError error = empty(); + error.code = DATA_NOT_FOUND; + return error; + } + + public boolean isDuplicateError() { + return code == DUPLICATE_ERROR; + } +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreEvent.java b/library/src/main/java/com/chwl/library/coremanager/CoreEvent.java new file mode 100644 index 0000000..55a28e3 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreEvent.java @@ -0,0 +1,23 @@ +/** + * 用于使用annotation实现监听某个client的某个回调 + */ +package com.chwl.library.coremanager; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @auth zhongyongsheng + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface CoreEvent { + + Class coreClientClass(); +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreEventListener.java b/library/src/main/java/com/chwl/library/coremanager/CoreEventListener.java new file mode 100644 index 0000000..b69fee9 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreEventListener.java @@ -0,0 +1,31 @@ +/** + * 用于使用annotation实现监听某个client的某个回调 + */ +package com.chwl.library.coremanager; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author daixiang + * + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface CoreEventListener { + + //String eventSource(); + + Class eventClass(); + + /** + * 所监听的接口的方法名 + * @return + */ + String eventName(); +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreException.java b/library/src/main/java/com/chwl/library/coremanager/CoreException.java new file mode 100644 index 0000000..dafe457 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreException.java @@ -0,0 +1,36 @@ +/** + * 用于封装core层的异常 + */ +package com.chwl.library.coremanager; + +/** + * @author daixiang + * + */ +public class CoreException extends Exception { + + private CoreError error; + + private static final long serialVersionUID = 1L; + + public CoreException(CoreError coreError) { + super(coreError.message, coreError.throwable); + error = coreError; + } + + public CoreException(String detailMessage) { + super(detailMessage); + } + + public CoreException(Throwable throwable) { + super(throwable); + } + + public CoreException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public CoreError getError() { + return error; + } +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreFactory.java b/library/src/main/java/com/chwl/library/coremanager/CoreFactory.java new file mode 100644 index 0000000..d8a90ef --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreFactory.java @@ -0,0 +1,93 @@ +/** + * Core对象工厂。使用getCore前需要注册对应core接口实现类 + * 此类是非线程安全的,必须在主线程调用 + */ +package com.chwl.library.coremanager; + + +import com.chwl.library.utils.log.MLog; + +import java.util.HashMap; + +/** + * @author daixiang + * + */ +public class CoreFactory { + + private static final HashMap, IBaseCore> cores; + private static final HashMap, Class> coreClasses; + static { + cores = new HashMap<>(); + coreClasses = new HashMap<>(); + } + + /** + * 从工厂获取实现cls接口的对象实例 + * 该实例是使用registerCoreClass注册的实现类的对象 + * + * @param cls 必须是core接口类,不能是core实现类,否则会抛出异常 + * @return 如果生成对象失败,返回null + */ + public static T getCore(Class cls) { + + if (cls == null) { + return null; + } + try { + IBaseCore core = cores.get(cls); + if (core == null) { + Class implClass = coreClasses.get(cls); + if (implClass == null) { + if (cls.isInterface()) { + MLog.error("CoreFactory", "No registered core class for: " + cls.getName()); + throw new IllegalArgumentException("No registered core class for: " + cls.getName()); + } else { + MLog.error("CoreFactory", "Not interface core class for: " + cls.getName()); + throw new IllegalArgumentException("Not interface core class for: " + cls.getName()); + } + } else { + core = implClass.newInstance(); + } + + if (core != null) { + cores.put(cls, core); + //MLog.debug("CoreFactory", cls.getName() + " created: " + // + ((implClass != null) ? implClass.getName() : cls.getName())); + } + } + return (T)core; + } catch (Throwable e) { + MLog.error("CoreFactory", "getCore() failed for: " + cls.getName(), e); + } + return null; + } + + /** + * 注册某个接口实现类 + * @param coreInterface + * @param coreClass + */ + public static void registerCoreClass(Class coreInterface, Class coreClass) { + + if (coreInterface == null || coreClass == null) { + return; + } + + coreClasses.put(coreInterface, coreClass); + MLog.debug("CoreFactory", "registered class " + coreClass.getName() + " for core: " + coreInterface.getName()); + } + + /** + * 返回某个接口是否有注册实现类 + * @param coreInterface + * @return + */ + public static boolean hasRegisteredCoreClass(Class coreInterface) { + if (coreInterface == null) { + return false; + } else { + return coreClasses.containsKey(coreInterface); + } + } +} diff --git a/library/src/main/java/com/chwl/library/coremanager/CoreManager.java b/library/src/main/java/com/chwl/library/coremanager/CoreManager.java new file mode 100644 index 0000000..aaaed99 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/CoreManager.java @@ -0,0 +1,432 @@ +/** + * 管理core对象的类。外部应该使用此类的接口来获取某个core对象 + * 它使用CoreFactory来声成core对象实例。 + * 上层未注册core实现类话,注册一个默认实现(调用init函数) + * 此类不是线程安全的,应该只在主线程调用 + */ +package com.chwl.library.coremanager; + +import android.content.Context; +import android.text.TextUtils; + +import com.chwl.library.utils.log.MLog; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * @author daixiang + */ +public class CoreManager { + + public static final String TAG = "CoreManager"; + public static final String TAG_EVENT = "CoreManager_Event"; + + private static Map, CopyOnWriteArraySet> clients + = new HashMap<>(); + + private static Map, CopyOnWriteArraySet> coreEvents = new HashMap<>(); + + private static Map, Map> clientMethods + = new HashMap(); + + private static Map> coreEventMethods = new HashMap(); + + private static Context context; + + public CoreManager() { + } + + public static void init(String logDir) { + if(!TextUtils.isEmpty(logDir)) { + MLog.LogOptions options = new MLog.LogOptions(); + options.uniformTag = TAG; + MLog.initialize(logDir, options); + } + + MLog.info(TAG, "--------------------------------CoreManager init--------------------------------", new Object[0]); + } + + public static void onTerminate() { + MLog.close(); + } + + public static Context getContext() { + return context; + } + + + private static void addClientMethodsIfNeeded(Class clientClass) { + try { + Map methods = clientMethods.get(clientClass); + if (methods == null) { + methods = new HashMap(); + Method[] allMethods = clientClass.getMethods(); + for (Method m : allMethods) { + methods.put(m.getName(), m); + } + clientMethods.put(clientClass, methods); + } + } catch (Throwable throwable) { + MLog.error(TAG, throwable); + } + + } + + /** + * 监听某个接口的回调,监听者需要实现该接口 + * 注意在不需要回调时要用removeClient + * + * @param clientClass + * @param client + */ + public static void addClient(Class clientClass, ICoreClient client) { + + if (clientClass == null || client == null) { + return; + } + + CopyOnWriteArraySet clientList = clients.get(clientClass); + if (clientList == null) { + clientList = new CopyOnWriteArraySet(); + clients.put(clientClass, clientList); + } + + addClientMethodsIfNeeded(clientClass); + + if (clientList.contains(client)) { + return; + } + + clientList.add(client); + //MLog.verbose(TAG, "client(" + client + ") added for " + clientClass.getName()); + } + + @SuppressWarnings("unchecked") + private static void addClient(ICoreClient client, Class clientClass) { + if (clientClass == null) + return; + + Class[] interfaces = clientClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + if (ICoreClient.class.isAssignableFrom(interfaces[i])) { + Class intf = (Class) interfaces[i]; + CoreManager.addClient(intf, client); + //logger.info("client(" + client + ") added for " + clientClass.getName()); + } + } + + Class superClass = clientClass.getSuperclass(); + addClient(client, superClass); + } + + /** + * 监听所有client声明实现的ICoreClient的接口 + * + * @param client + */ + + public static void addClientICoreClient(ICoreClient client) { + + if (client == null) { + return; + } + + addClient(client, client.getClass()); + } + + /** + * 移除对象对某个接口的监听 + * + * @param clientClass + * @param client + */ + public static void removeClient(Class clientClass, ICoreClient client) { + + if (clientClass == null || client == null) { + return; + } + + Set clientList = clients.get(clientClass); + if (clientList == null) { + return; + } + + clientList.remove(client); + //MLog.verbose(TAG, "client(" + client + ") removed for " + clientClass.getName()); + } + + + /** + * 移除该对象所有监听接口 + * + * @param client + */ + public static void removeClientICoreClient(ICoreClient client) { + + if (client == null) { + return; + } + + Collection> c = clients.values(); + for (Set list : c) { + list.remove(client); + } + + //MLog.verbose(TAG, "client(" + client + ") removed from all"); + } + + /** + * TODO 增加Client,支持CoreEvent注解 + * + * @param client + */ + public static void addClient(Object client) { + //MLog.verbose(TAG_EVENT, "AddClient support CoreEvent : " + client); + if (client == null) { + MLog.warn(TAG_EVENT, "Don't give me a null client"); + return; + } + + if (client instanceof ICoreClient) { + //MLog.verbose(TAG_EVENT, client + " instanceof ICoreClient, add to ICoreClient"); + addClientICoreClient((ICoreClient) client); + } + + Class originalClass = client.getClass(); + if (originalClass == null) { + MLog.warn(TAG_EVENT, "Client.getClass() is null"); + return; + } + Method[] methods = originalClass.getMethods(); + + for (Method method : methods) { + CoreEvent event = method.getAnnotation(CoreEvent.class); + if (event != null) { + Class clientClass = event.coreClientClass(); + //MLog.verbose(TAG_EVENT, "Client =" + client + ", event=" + event + ",method=" + method.getName()); + if (clientClass != null) { + addCoreEvents(client, clientClass); + addCoreEventMethodsIfNeeded(client, clientClass, method); + } + } + } + } + + private static void addCoreEvents(Object client, Class clientClass) { + CopyOnWriteArraySet clients = coreEvents.get(clientClass); + if (clients == null) { + //MLog.verbose(TAG_EVENT, "Clients is null, create new set :" + clientClass); + clients = new CopyOnWriteArraySet<>(); + coreEvents.put(clientClass, clients); + } + + clients.add(client); + //MLog.verbose(TAG_EVENT, "Clients add client " + client + ",size=" + clients.size()); + } + + private static void addCoreEventMethodsIfNeeded(Object client, Class clientClass, /*Class originalClass*/Method m) { + Map methods = coreEventMethods.get(client); + if (methods == null) { + //MLog.verbose(TAG_EVENT, "Client " + client + ",Class " + clientClass + " methods null, create new one"); + methods = new HashMap(); + coreEventMethods.put(client, methods); + } + //MLog.verbose(TAG_EVENT, "Client=" + client + ",Class=" + clientClass + ",put method=" + m.getName()); + methods.put(m.getName(), m); + } + + /** + * TODO 移除该对象所有监听接口,支持CoreEvent + * + * @param client + */ + public static void removeClient(Object client) { + + if (client == null) { + return; + } + try { + if (client instanceof ICoreClient) { + /*if (isDebugSvc()) + MLog.verbose(TAG_EVENT, "Client is ICoreClient, remove core client method");*/ + removeClientICoreClient((ICoreClient) client); + } + + Collection> c = coreEvents.values(); + for (CopyOnWriteArraySet events : c) { + events.remove(client); + } + + coreEventMethods.remove(client); + } catch (Throwable throwable) { + MLog.error("CoreManager", "removeClient error! " + throwable); + } + + //MLog.verbose(TAG_EVENT, "client(" + client + ") removed from all"); + } + + /** + * 返回监听该接口的对象列表 + * + * @param clientClass + * @return + */ + public static Set getClients(Class clientClass) { + + if (clientClass == null) { + return null; + } + + CopyOnWriteArraySet clientList = clients.get(clientClass); + return clientList; + } + + public interface ICallBack { + void onCall(ICoreClient client); + } + + /** + * 执行回调接口 + * + * @param clientClass + * @param callBack + */ + public static void notifyClients(Class clientClass, ICallBack callBack) { + if (clientClass == null || callBack == null) { + return; + } + + Set clientList = CoreManager.getClients(clientClass); + if (clientList == null) { + if (clientList == null) { + return; + } + } + try { + for (ICoreClient client : clientList) { + callBack.onCall(client); + } + + } catch (Exception e) { + MLog.error(TAG, e.getMessage(), e); + } + } + + /** + * 回调所有监听了该接口的对象。methodName为回调的方法名 + * 注意:所有用addClient和addEventListener注册了此接口的对象都会被回调 + * 注意:methodName所指定函数的参数列表个数必须匹配。目前没有对参数类型严格检查,使用时要注意 + * + * @param clientClass + * @param methodName + * @param args + */ + public static void notifyClients(Class clientClass, String methodName, Object... args) { + notifyClientsCoreEvents(clientClass, methodName, args); + if (clientClass == null || methodName == null || methodName.length() == 0) { + return; + } + + Set clientList = CoreManager.getClients(clientClass); + if (clientList == null) { + return; + } + + try { + + Map methods = clientMethods.get(clientClass); + Method method = methods.get(methodName); + + if (method == null) { + MLog.error(TAG, "cannot find client method " + methodName + " for args[" + args.length + "]: " + Arrays.toString(args)); + return; + } else if (method.getParameterTypes() == null) { + MLog.error(TAG, "cannot find client method param:" + method.getParameterTypes() + " for args[" + args.length + "]: " + Arrays.toString(args)); + return; + } else if (method.getParameterTypes().length != args.length) { + MLog.error(TAG, "method " + methodName + " param number not matched: method(" + method.getParameterTypes().length + "), args(" + args.length + ")"); + return; + } + for (Object c : clientList) { + try { + method.invoke(c, args); + } catch (Throwable e) { + MLog.error(TAG, "Notify clients method invoke error.", e); + } + } + } catch (Throwable e) { + MLog.error(TAG, "Notify clients error.", e); + } + } + + /** + * TODO 广播CoreEvent注解事件 + * + * @param clientClass + * @param methodName + * @param args + */ + public static void notifyClientsCoreEvents(Class clientClass, String methodName, Object... args) { + + if (clientClass == null || methodName == null || methodName.length() == 0) { + return; + } + + Set clients = coreEvents.get(clientClass); + + if (clients == null) { + MLog.debug(TAG_EVENT, "core clients is null clientClz:%s", clientClass.getSimpleName()); + return; + } + + try { + for (Object c : clients) { + Map methods = coreEventMethods.get(c); + if (methods == null) { + continue; + } + Method method = methods.get(methodName); + Class[] types = null; + if (method != null) { + types = method.getParameterTypes();//减少创建小对象,减少timeout崩溃 + } + + if (method == null) { + continue; + }else if (types == null) { + MLog.error(TAG_EVENT, "Can't find " + c + " has method param null for args[" + args.length + "]: " + args); + continue; + } else if (types.length != args.length) { + MLog.error(TAG_EVENT, "Can't find " + c + " has Method " + methodName + + " param number not matched: method(" + types.length + + "), args(" + args.length + ")"); + continue; + } + + try { + method.invoke(c, args); + } catch (Throwable e) { + MLog.error(TAG_EVENT, "Notify core events method invoke error class=" + clientClass + + ",method=" + methodName + + ",args=" + args, e); + } + } + + } catch (Throwable e) { + MLog.error(TAG_EVENT, "Notify core events error class=" + clientClass + ",method=" + methodName + + ",args=" + args, e); + } + } + + + public static T getCore(Class cls) { + return CoreFactory.getCore(cls); + } + +} diff --git a/library/src/main/java/com/chwl/library/coremanager/IBaseCore.java b/library/src/main/java/com/chwl/library/coremanager/IBaseCore.java new file mode 100644 index 0000000..e84ee9f --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/IBaseCore.java @@ -0,0 +1,12 @@ +/** + * 所有的core接口必须继承此接口 + */ +package com.chwl.library.coremanager; + +/** + * @author daixiang + * + */ +public interface IBaseCore { + +} diff --git a/library/src/main/java/com/chwl/library/coremanager/ICoreClient.java b/library/src/main/java/com/chwl/library/coremanager/ICoreClient.java new file mode 100644 index 0000000..1f14e75 --- /dev/null +++ b/library/src/main/java/com/chwl/library/coremanager/ICoreClient.java @@ -0,0 +1,12 @@ +/** + * 所有的core client接口必须继承于此接口 + */ +package com.chwl.library.coremanager; + +/** + * @author daixiang + * + */ +public interface ICoreClient { + +} diff --git a/library/src/main/java/com/chwl/library/error/CrashCat.java b/library/src/main/java/com/chwl/library/error/CrashCat.java new file mode 100644 index 0000000..7d9c152 --- /dev/null +++ b/library/src/main/java/com/chwl/library/error/CrashCat.java @@ -0,0 +1,121 @@ +package com.chwl.library.error; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +//xxx 崩溃日志拦截 +public class CrashCat implements Thread.UncaughtExceptionHandler { + + private static CrashCat crashCat; + private Context mContext; + private Thread.UncaughtExceptionHandler mDefaultHandler; + private static String DEVICE_INFO=""; + private File outPutDir; + private FileOutputStream fileOutputStream; + private BufferedOutputStream bufferedOutputStream; + private static String FILE_NAME = ""; + private Intent intent; + private PackageManager packageManager; + private PackageInfo packageInfo; + + private CrashCat(Context context, String filePath, String fileName){ + init(context,filePath,fileName); + } + + public static CrashCat getInstance(Context context, String filePath, String fileName){ + crashCat = new CrashCat(context,filePath,fileName); + return crashCat; + } + + private void init(Context context, String filePath, String fileName){ + this.mContext = context; + this.FILE_NAME = fileName; + try { + packageManager = mContext.getPackageManager(); + packageInfo = packageManager.getPackageInfo(mContext.getPackageName(),0); + intent = packageManager.getLaunchIntentForPackage(mContext.getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + } catch (Exception e) { + writeLog(e.toString()); + intent = null; + } + + outPutDir = context.getExternalFilesDir(null); + if (outPutDir != null){ + if (!outPutDir.exists()) { + outPutDir.mkdirs(); + } + } + + StringBuffer sb = new StringBuffer(); + sb.append("DeviceID="+ Build.ID+"\n"); + sb.append("AndroidApi="+ Build.VERSION.SDK_INT+"\n"); + sb.append("AndroidVersion="+ Build.VERSION.RELEASE+"\n"); + sb.append("Brand="+ Build.BRAND+"\n"); + sb.append("ManuFacture="+ Build.MANUFACTURER+"\n"); + sb.append("Model="+ Build.MODEL+"\n"); + sb.append("PackageName="+mContext.getPackageName()+"\n"); + sb.append("CurrentVersionName="+packageInfo.versionName+"\n"); + DEVICE_INFO = sb.toString(); +// writeLog("Application Start"); + } + + public void start(){ + mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + } + + private void writeLog(String log){ + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + SimpleDateFormat fileTime = new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss"); + log = "----------"+simpleDateFormat.format(new Date(System.currentTimeMillis())).toString()+"----------"+"\n"+log+"\n"; + try { + + File outPutFile = new File(outPutDir, "error-"+fileTime.format(new Date(System.currentTimeMillis()))+".txt"); + fileOutputStream = new FileOutputStream(outPutFile,true); + bufferedOutputStream = new BufferedOutputStream(fileOutputStream); + bufferedOutputStream.write(log.getBytes()); + bufferedOutputStream.flush(); + fileOutputStream.close(); + bufferedOutputStream.close(); + } catch (Exception e) { + Log.e("IO Exception",e.toString()); + } + } + + private void handlerException(String exception) { + if (exception !=null){ + try{ + writeLog(DEVICE_INFO+exception.toString()); + }finally { + try{ + mContext.startActivity(intent); + System.exit(1); + }catch (Exception e){ + Log.e("App can not restart",e.toString()); + } + } + } + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + StackTraceElement[] stackTraceElements = e.getStackTrace(); + StringBuffer sb = new StringBuffer(e.toString()+"\n"); + for (int i=0,size = stackTraceElements.length;i= Build.VERSION_CODES.N) { + LocaleList.getDefault()[0] + } else { + Locale.getDefault() + } + } + + /** + * 当前语言 + */ + fun getCurrentLanguage(): Locale { + var locale = currentLocale + if (locale != null) { + return locale + } + val language = SPUtils.getString("language", "") + locale = buildLocaleByLanguageTag( + language, + limitSupportedLocale(getSystemLanguage(), Locale.ENGLISH) + ) + currentLocale = locale + return locale + } + + /** + * 获取当前语言类型(四端一致的语言类型) + */ + fun getCurrentLanguageType(): String { + val locale = getCurrentLanguage() + return if (locale == Locale.ENGLISH) { + EN + } else if (locale == Locale.TRADITIONAL_CHINESE) { + ZH + } else if (locale.language.equals("ar", true)) { + return AR + } else if (locale.language.equals("tr", true)) { + return TR + } else { + EN + } + } + + fun getH5GameLeaderccLan(): String { + val locale = getCurrentLanguage() + return if (locale == Locale.ENGLISH) { + "en-US" + } else if (locale == Locale.TRADITIONAL_CHINESE) { + "zh-TW" + } else if (locale.language.equals("ar", true)) { + return "ar-EG" + } else if (locale.language.equals("tr", true)) { + return "tr-TR" + } else { + "en-Us" + } + } + + /** + * 获取Locale + * @param language (四端一致的类型) + */ + fun getLocaleByLanguageType(language: String?): Locale? { + return when (language) { + ZH -> { + Locale.TRADITIONAL_CHINESE + } + + EN -> { + Locale.ENGLISH + } + + AR -> { + Locale("ar") + } + TR -> { + Locale("tr") + } + + else -> { + null + } + } + } + + /** + * 修改语言配置 + */ + fun changeLanguageConfig(locale: Locale) { + SPUtils.putString("language", locale.toLanguageTag()) + } + + /** + * 切换语言 + */ + fun changeLanguage(context: Context, locale: Locale): Context { + changeResources(context, locale) + currentLocale = locale + Locale.setDefault(locale) + val resources: Resources = context.resources + val metrics = resources.displayMetrics + val configuration = resources.configuration + configuration.setLayoutDirection(locale) + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { + configuration.setLocale(locale) + configuration.setLocales(LocaleList(locale)) + resources.updateConfiguration(configuration, metrics) + context.createConfigurationContext(configuration) + } + + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> { + configuration.setLocale(locale) + resources.updateConfiguration(configuration, metrics) + context + } + + else -> { + configuration.locale = locale + resources.updateConfiguration(configuration, metrics) + context + } + } + } + + /** + * 改变资源 + */ + private fun changeResources(context: Context, locale: Locale): Resources? { + try { + val res = context.resources ?: return null + val configuration = Configuration(res.configuration) + configuration.setLocale(locale) + return Resources(res.assets, res.displayMetrics, configuration) + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + /** + * 配置Context多语言 + */ + fun wrapContext(context: Context?): Context? { + if (context == null) return null + val currentLanguage = getCurrentLanguage() + return changeLanguage(context, currentLanguage) + } + + /** + * 根据国家语言TAG构建Locale + */ + private fun buildLocaleByLanguageTag(languageTag: String, defaultLocale: Locale): Locale { + return try { + if (languageTag.isEmpty()) { + return defaultLocale + } + limitSupportedLocale(Locale.forLanguageTag(languageTag), defaultLocale) + } catch (e: Exception) { + defaultLocale + } + } + + /** + * 限制为支持的语言 + */ + private fun limitSupportedLocale(locale: Locale, defaultLocale: Locale): Locale { + logI( + "limitSupportedLocale() language:${locale.language} country:${locale.country} toLanguageTag:${locale.toLanguageTag()}", + filePrinter = true + ) + when (locale.language) { + AR -> { + return Locale("ar") + } + TR -> { + return Locale("tr") + } + + EN -> { + return Locale.ENGLISH + } + + ZH -> { + return Locale.TRADITIONAL_CHINESE + } + + else -> { + return defaultLocale + } + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/list/AnimatedExpandableListView.java b/library/src/main/java/com/chwl/library/list/AnimatedExpandableListView.java new file mode 100644 index 0000000..b71b04a --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/AnimatedExpandableListView.java @@ -0,0 +1,606 @@ +package com.chwl.library.list; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.Transformation; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListAdapter; +import android.widget.ExpandableListView; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class defines an ExpandableListView which supports animations for + * collapsing and expanding groups. + */ +public class AnimatedExpandableListView extends ExpandableListView { + /* + * A detailed explanation for how this class works: + * + * Animating the ExpandableListView was no easy task. The way that this + * class does it is by exploiting how an ExpandableListView works. + * + * Normally when {@link ExpandableListView#collapseGroup(int)} or + * {@link ExpandableListView#expandGroup(int)} is called, the view toggles + * the flag for a group and calls notifyDataSetChanged to cause the ListView + * to refresh all of it's view. This time however, depending on whether a + * group is expanded or collapsed, certain childViews will either be ignored + * or added to the list. + * + * Knowing this, we can come up with a way to animate our views. For + * instance for group expansion, we tell the adapter to animate the + * children of a certain group. We then expand the group which causes the + * ExpandableListView to refresh all views on screen. The way that + * ExpandableListView does this is by calling getView() in the adapter. + * However since the adapter knows that we are animating a certain group, + * instead of returning the real views for the children of the group being + * animated, it will return a fake dummy view. This dummy view will then + * draw the real child views within it's dispatchDraw function. The reason + * we do this is so that we can animate all of it's children by simply + * animating the dummy view. After we complete the animation, we tell the + * adapter to stop animating the group and call notifyDataSetChanged. Now + * the ExpandableListView is forced to refresh it's views again, except this + * time, it will get the real views for the expanded group. + * + * So, to list it all out, when {@link #expandGroupWithAnimation(int)} is + * called the following happens: + * + * 1. The ExpandableListView tells the adapter to animate a certain group. + * 2. The ExpandableListView calls expandGroup. + * 3. ExpandGroup calls notifyDataSetChanged. + * 4. As an result, getChildView is called for expanding group. + * 5. Since the adapter is in "animating mode", it will return a dummy view. + * 6. This dummy view draws the actual children of the expanding group. + * 7. This dummy view's height is animated from 0 to it's expanded height. + * 8. Once the animation completes, the adapter is notified to stop + * animating the group and notifyDataSetChanged is called again. + * 9. This forces the ExpandableListView to refresh all of it's views again. + * 10.This time when getChildView is called, it will return the actual + * child views. + * + * For animating the collapse of a group is a bit more difficult since we + * can't call collapseGroup from the start as it would just ignore the + * child items, giving up no chance to do any sort of animation. Instead + * what we have to do is play the animation first and call collapseGroup + * after the animation is done. + * + * So, to list it all out, when {@link #collapseGroupWithAnimation(int)} is + * called the following happens: + * + * 1. The ExpandableListView tells the adapter to animate a certain group. + * 2. The ExpandableListView calls notifyDataSetChanged. + * 3. As an result, getChildView is called for expanding group. + * 4. Since the adapter is in "animating mode", it will return a dummy view. + * 5. This dummy view draws the actual children of the expanding group. + * 6. This dummy view's height is animated from it's current height to 0. + * 7. Once the animation completes, the adapter is notified to stop + * animating the group and notifyDataSetChanged is called again. + * 8. collapseGroup is finally called. + * 9. This forces the ExpandableListView to refresh all of it's views again. + * 10.This time when the ListView will not get any of the child views for + * the collapsed group. + */ + + @SuppressWarnings("unused") + private static final String TAG = AnimatedExpandableListAdapter.class.getSimpleName(); + + /** + * The duration of the expand/collapse animations + */ + private static final int ANIMATION_DURATION = 300; + + private AnimatedExpandableListAdapter adapter; + + public AnimatedExpandableListView(Context context) { + super(context); + } + + public AnimatedExpandableListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AnimatedExpandableListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * @see ExpandableListView#setAdapter(ExpandableListAdapter) + */ + public void setAdapter(ExpandableListAdapter adapter) { + super.setAdapter(adapter); + + // Make sure that the adapter extends AnimatedExpandableListAdapter + if (adapter instanceof AnimatedExpandableListAdapter) { + this.adapter = (AnimatedExpandableListAdapter) adapter; + this.adapter.setParent(this); + } else { + throw new ClassCastException(adapter.toString() + " must implement AnimatedExpandableListAdapter"); + } + } + + /** + * Expands the given group with an animation. + * + * @param groupPos The position of the group to expand + * @return Returns true if the group was expanded. False if the group was + * already expanded. + */ + @SuppressLint("NewApi") + public boolean expandGroupWithAnimation(int groupPos) { + boolean lastGroup = groupPos == adapter.getGroupCount() - 1; + if (lastGroup && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return expandGroup(groupPos, true); + } + + int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos)); + if (groupFlatPos != -1) { + int childIndex = groupFlatPos - getFirstVisiblePosition(); + if (childIndex < getChildCount()) { + // Get the view for the group is it is on screen... + View v = getChildAt(childIndex); + if (v.getBottom() >= getBottom()) { + // If the user is not going to be able to see the animation + // we just expand the group without an animation. + // This resolves the case where getChildView will not be + // called if the children of the group is not on screen + + // We need to notify the adapter that the group was expanded + // without it's knowledge + adapter.notifyGroupExpanded(groupPos); + return expandGroup(groupPos); + } + } + } + + // Let the adapter know that we are starting the animation... + adapter.startExpandAnimation(groupPos, 0); + // Finally call expandGroup (note that expandGroup will call + // notifyDataSetChanged so we don't need to) + return expandGroup(groupPos); + } + + /** + * Collapses the given group with an animation. + * + * @param groupPos The position of the group to collapse + * @return Returns true if the group was collapsed. False if the group was + * already collapsed. + */ + public boolean collapseGroupWithAnimation(int groupPos) { + int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos)); + if (groupFlatPos != -1) { + int childIndex = groupFlatPos - getFirstVisiblePosition(); + if (childIndex >= 0 && childIndex < getChildCount()) { + // Get the view for the group is it is on screen... + View v = getChildAt(childIndex); + if (v.getBottom() >= getBottom()) { + // If the user is not going to be able to see the animation + // we just collapse the group without an animation. + // This resolves the case where getChildView will not be + // called if the children of the group is not on screen + return collapseGroup(groupPos); + } + } else { + // If the group is offscreen, we can just collapse it without an + // animation... + return collapseGroup(groupPos); + } + } + + // Get the position of the firstChild visible from the top of the screen + long packedPos = getExpandableListPosition(getFirstVisiblePosition()); + int firstChildPos = getPackedPositionChild(packedPos); + int firstGroupPos = getPackedPositionGroup(packedPos); + + // If the first visible view on the screen is a child view AND it's a + // child of the group we are trying to collapse, then set that + // as the first child position of the group... see + // {@link #startCollapseAnimation(int, int)} for why this is necessary + firstChildPos = firstChildPos == -1 || firstGroupPos != groupPos ? 0 : firstChildPos; + + // Let the adapter know that we are going to start animating the + // collapse animation. + adapter.startCollapseAnimation(groupPos, firstChildPos); + + // Force the listview to refresh it's views + adapter.notifyDataSetChanged(); + return isGroupExpanded(groupPos); + } + + private int getAnimationDuration() { + return ANIMATION_DURATION; + } + + /** + * Used for holding information regarding the group. + */ + private static class GroupInfo { + boolean animating = false; + boolean expanding = false; + int firstChildPosition; + + /** + * This variable contains the last known height value of the dummy view. + * We save this information so that if the user collapses a group + * before it fully expands, the collapse animation will start from the + * CURRENT height of the dummy view and not from the full expanded + * height. + */ + int dummyHeight = -1; + } + + /** + * A specialized adapter for use with the AnimatedExpandableListView. All + * adapters used with AnimatedExpandableListView MUST extend this class. + */ + public static abstract class AnimatedExpandableListAdapter extends BaseExpandableListAdapter { + private SparseArray groupInfo = new SparseArray(); + private AnimatedExpandableListView parent; + + private static final int STATE_IDLE = 0; + private static final int STATE_EXPANDING = 1; + private static final int STATE_COLLAPSING = 2; + + private void setParent(AnimatedExpandableListView parent) { + this.parent = parent; + } + + public int getRealChildType(int groupPosition, int childPosition) { + return 0; + } + + public int getRealChildTypeCount() { + return 1; + } + + public abstract View getRealChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent); + + public abstract int getRealChildrenCount(int groupPosition); + + private GroupInfo getGroupInfo(int groupPosition) { + GroupInfo info = groupInfo.get(groupPosition); + if (info == null) { + info = new GroupInfo(); + groupInfo.put(groupPosition, info); + } + return info; + } + + public void notifyGroupExpanded(int groupPosition) { + GroupInfo info = getGroupInfo(groupPosition); + info.dummyHeight = -1; + } + + private void startExpandAnimation(int groupPosition, int firstChildPosition) { + GroupInfo info = getGroupInfo(groupPosition); + info.animating = true; + info.firstChildPosition = firstChildPosition; + info.expanding = true; + } + + private void startCollapseAnimation(int groupPosition, int firstChildPosition) { + GroupInfo info = getGroupInfo(groupPosition); + info.animating = true; + info.firstChildPosition = firstChildPosition; + info.expanding = false; + } + + private void stopAnimation(int groupPosition) { + GroupInfo info = getGroupInfo(groupPosition); + info.animating = false; + } + + /** + * Override {@link #getRealChildType(int, int)} instead. + */ + @Override + public final int getChildType(int groupPosition, int childPosition) { + GroupInfo info = getGroupInfo(groupPosition); + if (info.animating) { + // If we are animating this group, then all of it's children + // are going to be dummy views which we will say is type 0. + return 0; + } else { + // If we are not animating this group, then we will add 1 to + // the type it has so that no type id conflicts will occur + // unless getRealChildType() returns MAX_INT + return getRealChildType(groupPosition, childPosition) + 1; + } + } + + /** + * Override {@link #getRealChildTypeCount()} instead. + */ + @Override + public final int getChildTypeCount() { + // Return 1 more than the childTypeCount to account for DummyView + return getRealChildTypeCount() + 1; + } + + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + + /** + * Override {@link #getChildView(int, int, boolean, View, ViewGroup)} instead. + */ + @Override + public final View getChildView(final int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) { + final GroupInfo info = getGroupInfo(groupPosition); + + if (info.animating) { + // If this group is animating, return the a DummyView... + if (convertView instanceof DummyView == false) { + convertView = new DummyView(parent.getContext()); + convertView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 0)); + } + + if (childPosition < info.firstChildPosition) { + // The reason why we do this is to support the collapse + // this group when the group view is not visible but the + // children of this group are. When notifyDataSetChanged + // is called, the ExpandableListView tries to keep the + // list position the same by saving the first visible item + // and jumping back to that item after the views have been + // refreshed. Now the problem is, if a group has 2 items + // and the first visible item is the 2nd child of the group + // and this group is collapsed, then the dummy view will be + // used for the group. But now the group only has 1 item + // which is the dummy view, thus when the ListView is trying + // to restore the scroll position, it will try to jump to + // the second item of the group. But this group no longer + // has a second item, so it is forced to jump to the next + // group. This will cause a very ugly visual glitch. So + // the way that we counteract this is by creating as many + // dummy views as we need to maintain the scroll position + // of the ListView after notifyDataSetChanged has been + // called. + convertView.getLayoutParams().height = 0; + return convertView; + } + + final ExpandableListView listView = (ExpandableListView) parent; + + final DummyView dummyView = (DummyView) convertView; + + // Clear the views that the dummy view draws. + dummyView.clearViews(); + + // Set the style of the divider + dummyView.setDivider(listView.getDivider(), parent.getMeasuredWidth(), listView.getDividerHeight()); + + // Make measure specs to measure child views + final int measureSpecW = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY); + final int measureSpecH = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + int totalHeight = 0; + int clipHeight = parent.getHeight(); + + final int len = getRealChildrenCount(groupPosition); + for (int i = info.firstChildPosition; i < len; i++) { + View childView = getRealChildView(groupPosition, i, (i == len - 1), null, parent); + + LayoutParams p = (LayoutParams) childView.getLayoutParams(); + if (p == null) { + p = (LayoutParams) generateDefaultLayoutParams(); + childView.setLayoutParams(p); + } + + int lpHeight = p.height; + + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = measureSpecH; + } + + childView.measure(measureSpecW, childHeightSpec); + totalHeight += childView.getMeasuredHeight(); + + if (totalHeight < clipHeight) { + // we only need to draw enough views to fool the user... + dummyView.addFakeView(childView); + } else { + dummyView.addFakeView(childView); + + // if this group has too many views, we don't want to + // calculate the height of everything... just do a light + // approximation and break + int averageHeight = totalHeight / (i + 1); + totalHeight += (len - i - 1) * averageHeight; + break; + } + } + + Object o; + int state = (o = dummyView.getTag()) == null ? STATE_IDLE : (Integer) o; + + if (info.expanding && state != STATE_EXPANDING) { + ExpandAnimation ani = new ExpandAnimation(dummyView, 0, totalHeight, info); + ani.setDuration(this.parent.getAnimationDuration()); + ani.setAnimationListener(new AnimationListener() { + + @Override + public void onAnimationEnd(Animation animation) { + stopAnimation(groupPosition); + notifyDataSetChanged(); + dummyView.setTag(STATE_IDLE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + + }); + dummyView.startAnimation(ani); + dummyView.setTag(STATE_EXPANDING); + } else if (!info.expanding && state != STATE_COLLAPSING) { + if (info.dummyHeight == -1) { + info.dummyHeight = totalHeight; + } + + ExpandAnimation ani = new ExpandAnimation(dummyView, info.dummyHeight, 0, info); + ani.setDuration(this.parent.getAnimationDuration()); + ani.setAnimationListener(new AnimationListener() { + + @Override + public void onAnimationEnd(Animation animation) { + stopAnimation(groupPosition); + listView.collapseGroup(groupPosition); + notifyDataSetChanged(); + info.dummyHeight = -1; + dummyView.setTag(STATE_IDLE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + + }); + dummyView.startAnimation(ani); + dummyView.setTag(STATE_COLLAPSING); + } + + return convertView; + } else { + return getRealChildView(groupPosition, childPosition, isLastChild, convertView, parent); + } + } + + @Override + public final int getChildrenCount(int groupPosition) { + GroupInfo info = getGroupInfo(groupPosition); + if (info.animating) { + return info.firstChildPosition + 1; + } else { + return getRealChildrenCount(groupPosition); + } + } + + } + + private static class DummyView extends View { + private List views = new ArrayList(); + private Drawable divider; + private int dividerWidth; + private int dividerHeight; + + public DummyView(Context context) { + super(context); + } + + public void setDivider(Drawable divider, int dividerWidth, int dividerHeight) { + if (divider != null) { + this.divider = divider; + this.dividerWidth = dividerWidth; + this.dividerHeight = dividerHeight; + + divider.setBounds(0, 0, dividerWidth, dividerHeight); + } + } + + /** + * Add a view for the DummyView to draw. + * + * @param childView View to draw + */ + public void addFakeView(View childView) { + childView.layout(0, 0, getWidth(), childView.getMeasuredHeight()); + views.add(childView); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + final int len = views.size(); + for (int i = 0; i < len; i++) { + View v = views.get(i); + v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); + } + } + + public void clearViews() { + views.clear(); + } + + @Override + public void dispatchDraw(Canvas canvas) { + canvas.save(); + if (divider != null) { + divider.setBounds(0, 0, dividerWidth, dividerHeight); + } + + final int len = views.size(); + for (int i = 0; i < len; i++) { + View v = views.get(i); + + canvas.save(); + canvas.clipRect(0, 0, getWidth(), v.getMeasuredHeight()); + v.draw(canvas); + canvas.restore(); + + if (divider != null) { + divider.draw(canvas); + canvas.translate(0, dividerHeight); + } + + canvas.translate(0, v.getMeasuredHeight()); + } + + canvas.restore(); + } + } + + private static class ExpandAnimation extends Animation { + private int baseHeight; + private int delta; + private View view; + private GroupInfo groupInfo; + + private ExpandAnimation(View v, int startHeight, int endHeight, GroupInfo info) { + baseHeight = startHeight; + delta = endHeight - startHeight; + view = v; + groupInfo = info; + + view.getLayoutParams().height = startHeight; + view.requestLayout(); + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + super.applyTransformation(interpolatedTime, t); + if (interpolatedTime < 1.0f) { + int val = baseHeight + (int) (delta * interpolatedTime); + view.getLayoutParams().height = val; + groupInfo.dummyHeight = val; + view.requestLayout(); + } else { + int val = baseHeight + delta; + view.getLayoutParams().height = val; + groupInfo.dummyHeight = val; + view.requestLayout(); + } + } + } +} diff --git a/library/src/main/java/com/chwl/library/list/ArrayListAdapter.java b/library/src/main/java/com/chwl/library/list/ArrayListAdapter.java new file mode 100644 index 0000000..2ce03df --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/ArrayListAdapter.java @@ -0,0 +1,88 @@ +package com.chwl.library.list; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by lijun on 2014/11/12. + * Modified by wjc133 on 2015/8/27. + */ +public class ArrayListAdapter extends BaseListAdapter { + + private boolean mNotifyOnChange = true; + private final Object mLock = new Object(); + private List items = new ArrayList<>(); + + + public void addItem(ListItem item) { + synchronized (mLock) { + items.add(item); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + public void addItems(List items) { + synchronized (mLock) { + this.items.addAll(items); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + public void addItems(ListItem... items) { + synchronized (mLock) { + Collections.addAll(this.items, items); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + public void insert(ListItem item, int index) { + synchronized (mLock) { + items.add(index, item); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + public void remove(ListItem object) { + synchronized (mLock) { + if (items.contains(object)) { + items.remove(object); + } + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + public void clear() { + synchronized (mLock) { + items.clear(); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + mNotifyOnChange = true; + } + + public void setNotifyOnChange(boolean notifyOnChange) { + mNotifyOnChange = notifyOnChange; + } + + public ListItem getItem(int position) { + if (position >= 0 && position < items.size()) { + return items.get(position); + } + return null; + } + + protected List getItems() { + return items; + } + + @Override + public int getCount() { + return items.size(); + } + +} diff --git a/library/src/main/java/com/chwl/library/list/BaseAnimatedExpandableListAdapter.java b/library/src/main/java/com/chwl/library/list/BaseAnimatedExpandableListAdapter.java new file mode 100644 index 0000000..8075748 --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/BaseAnimatedExpandableListAdapter.java @@ -0,0 +1,107 @@ +package com.chwl.library.list; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Created by lijun on 2014/11/11. + */ +public abstract class BaseAnimatedExpandableListAdapter extends AnimatedExpandableListView.AnimatedExpandableListAdapter { + + @Override + public abstract int getGroupCount(); + + @Override + public abstract GroupItem getGroup(int groupPosition); + + @Override + public ListItem getChild(int groupPosition, int childPosition) { + GroupItem groupItem = getGroup(groupPosition); + if (null != groupItem.getChildItems()) { + return groupItem.getChildItems().get(childPosition); + } + + return null; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public View getRealChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { +// Log.d("BaseAnimatedExpandableListAdapter:", "getRealChildView, groupPosition:" + groupPosition + +// ", childPosition:" + childPosition + " convertView:" + convertView); + + final ViewHolder holder; + ListItem childItem = getChild(groupPosition, childPosition); + if (null == childItem) { + throw new RuntimeException("list item is never null. pos:" + groupPosition); + } + + if (null == convertView) { + holder = createViewHolder(parent, childItem); + convertView = holder.itemView; + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + childItem.updateHolder(holder, groupPosition, childPosition); + return convertView; + } + + @Override + public int getRealChildrenCount(int groupPosition) { + GroupItem groupItem = getGroup(groupPosition); + if (null != groupItem.getChildItems()) { + return groupItem.getChildItems().size(); + } + + return 0; + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { +// Log.d("BaseAnimatedExpandableListAdapter:", "getGroupView:" + groupPosition + " convertView:" + convertView); + + final ViewHolder holder; + final ListItem item = getGroup(groupPosition); + + if (null == item) { + throw new RuntimeException("list item is never null. pos:" + groupPosition); + } + + if (null == convertView) { + holder = createViewHolder(parent, item); + convertView = holder.itemView; + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + item.updateHolder(holder, groupPosition, -1); + return convertView; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean isChildSelectable(int arg0, int arg1) { + return true; + } + + private ViewHolder createViewHolder(ViewGroup group, ListItem item) { + return item.createViewHolder(group); + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/list/BaseGroupItem.java b/library/src/main/java/com/chwl/library/list/BaseGroupItem.java new file mode 100644 index 0000000..e8dd405 --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/BaseGroupItem.java @@ -0,0 +1,81 @@ +package com.chwl.library.list; + +import android.content.Context; +import android.util.Log; +import android.view.View; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by lijun on 2014/11/7. + */ +public abstract class BaseGroupItem extends BaseListItem implements GroupItem { + + protected OnIndicatorClickListener mIndicatorListener; + protected int mGroupPos, mChildPos; + protected List mChildItems = new ArrayList(1); + + public BaseGroupItem(Context mContext) { + super(mContext); + } + + public BaseGroupItem(Context mContext, int viewType) { + super(mContext, viewType); + } + + public void setMoreItem(ListItem item) { + mChildItems.add(0, item); + } + + @Override + public List getChildItems() { + return mChildItems; + } + + public static List createLineItems(Context context, Class lineItemClazz, List list, Class voClazz, Integer viewType, Integer column) { + List items = new ArrayList(); + try { + if (list != null) { + int size = list.size(); + int rows = size % column == 0 ? size / column : size / column + 1; + for (int i = 0; i < rows; i++) { + T[] albums; + if (i != rows - 1) { + albums = (T[]) Array.newInstance(voClazz, column); + for (int index = 0; index < column; ++index) { + albums[index] = list.get(i * column + index); + } + } else { + albums = (T[]) Array.newInstance(voClazz, column); + int j = 0; + while (size > i * column + j) { + albums[j] = list.get(i * column + j++); + } + } + + Constructor constructor = lineItemClazz.getDeclaredConstructor(Context.class, Integer.class, albums.getClass()); + E lineItem = constructor.newInstance(context, viewType, albums); + items.add(lineItem); + } + } + } catch (NoSuchMethodException e) { + Log.e("BaseGroupItem:", "createLineItems NoSuchMethodException", e); + } catch (InvocationTargetException e) { + Log.e("BaseGroupItem:", "createLineItems InvocationTargetException", e); + } catch (InstantiationException e) { + Log.e("BaseGroupItem:", "createLineItems InstantiationException", e); + } catch (IllegalAccessException e) { + Log.e("BaseGroupItem:", "createLineItems IllegalAccessException", e); + } + + return items; + } + + public static interface OnIndicatorClickListener { + void onClick(View v, int groupPos, int childPos); + } +} diff --git a/library/src/main/java/com/chwl/library/list/BaseListAdapter.java b/library/src/main/java/com/chwl/library/list/BaseListAdapter.java new file mode 100644 index 0000000..6044a4f --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/BaseListAdapter.java @@ -0,0 +1,64 @@ +package com.chwl.library.list; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * Created by lijun on 2014/11/12. + */ +public abstract class BaseListAdapter extends BaseAdapter { + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public abstract ListItem getItem(int position); + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public int getItemViewType(int position) { + return getItem(position).getViewType(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final ViewHolder holder; + final ListItem item = getItem(position); + + if (null == item) { + throw new RuntimeException("list item is never null. pos:" + position); + } + + if (null == convertView) { + holder = createViewHolder(parent, item); + convertView = holder.itemView; + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + item.updateHolder(holder, position, -1); + return convertView; + } + + private ViewHolder createViewHolder(ViewGroup group, ListItem item) { + return item.createViewHolder(group); + } + + @Override + public boolean isEnabled(int position) { + ListItem item = getItem(position); + if (null != item) { + return item.isEnabled(); + } + + return super.isEnabled(position); + } +} diff --git a/library/src/main/java/com/chwl/library/list/BaseListItem.java b/library/src/main/java/com/chwl/library/list/BaseListItem.java new file mode 100644 index 0000000..065f417 --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/BaseListItem.java @@ -0,0 +1,61 @@ +package com.chwl.library.list; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * Creator: 舒强睿 + * Date:2015/4/24 + * Time:19:03 + *

+ * Description: + */ +public class BaseListItem implements ListItem { + + protected Context mContext; + protected int viewType; + protected boolean isSelected; + + public BaseListItem(Context mContext) { + this.mContext = mContext; + } + + public BaseListItem(Context mContext, int viewType) { + this.mContext = mContext; + this.viewType = viewType; + } + + protected Context getContext() { + return mContext; + } + + + @Override + public ViewHolder createViewHolder(ViewGroup group) { + return null; + } + + @Override + public void updateHolder(ViewHolder holder, int groupPos, int childPos) { + + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public int getViewType() { + return viewType; + } + + @Override + public boolean isSelected() { + return isSelected; + } + + public void setSelected(boolean isSelected) { + this.isSelected = isSelected; + } +} diff --git a/library/src/main/java/com/chwl/library/list/GroupItem.java b/library/src/main/java/com/chwl/library/list/GroupItem.java new file mode 100644 index 0000000..c363d9a --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/GroupItem.java @@ -0,0 +1,11 @@ +package com.chwl.library.list; + +import java.util.List; + +/** + * Created by lijun on 2014/11/7. + */ +public interface GroupItem extends ListItem { + + public List getChildItems(); +} diff --git a/library/src/main/java/com/chwl/library/list/ListItem.java b/library/src/main/java/com/chwl/library/list/ListItem.java new file mode 100644 index 0000000..ad782d8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/ListItem.java @@ -0,0 +1,20 @@ +package com.chwl.library.list; + +import android.view.ViewGroup; + +/** + * Created by lijun on 2014/11/7. + */ +public interface ListItem { + + ViewHolder createViewHolder(ViewGroup group); + + void updateHolder(ViewHolder holder, int groupPos, int childPos); + + boolean isEnabled(); + + boolean isSelected(); + + int getViewType(); + +} diff --git a/library/src/main/java/com/chwl/library/list/NoScrollingLinearLayoutManager.java b/library/src/main/java/com/chwl/library/list/NoScrollingLinearLayoutManager.java new file mode 100644 index 0000000..b9f0824 --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/NoScrollingLinearLayoutManager.java @@ -0,0 +1,33 @@ +package com.chwl.library.list; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.recyclerview.widget.LinearLayoutManager; + +public class NoScrollingLinearLayoutManager extends LinearLayoutManager { + + private boolean isScrollEnabled = true; + + public NoScrollingLinearLayoutManager(Context context) { + super(context); + } + + public NoScrollingLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + public NoScrollingLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setScrollEnabled(boolean flag) { + this.isScrollEnabled = flag; + } + + @Override + public boolean canScrollVertically() { + //Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll + return isScrollEnabled && super.canScrollVertically(); + } +} diff --git a/library/src/main/java/com/chwl/library/list/ViewHolder.java b/library/src/main/java/com/chwl/library/list/ViewHolder.java new file mode 100644 index 0000000..d740dff --- /dev/null +++ b/library/src/main/java/com/chwl/library/list/ViewHolder.java @@ -0,0 +1,18 @@ +package com.chwl.library.list; + +import android.view.View; + +/** + * Created by lijun on 2014/11/7. + */ +public abstract class ViewHolder { + + public final View itemView; + + public ViewHolder(View itemView) { + if (itemView == null) { + throw new IllegalArgumentException("itemView may not be null"); + } + this.itemView = itemView; + } +} diff --git a/library/src/main/java/com/chwl/library/manager/TracingManager.java b/library/src/main/java/com/chwl/library/manager/TracingManager.java new file mode 100644 index 0000000..ed77603 --- /dev/null +++ b/library/src/main/java/com/chwl/library/manager/TracingManager.java @@ -0,0 +1,42 @@ +package com.chwl.library.manager; + +import android.app.Application; +import android.os.Debug; + +import androidx.annotation.NonNull; + +/** + *

+ * + * @author jiahui + * @date 2018/1/26 + */ +public class TracingManager { + + + private boolean mDebug; + + private static class TracingManagerHelper { + private static final TracingManager INSTANCE = new TracingManager(); + } + + + public static TracingManager get() { + return TracingManagerHelper.INSTANCE; + } + + public void init(Application application, boolean debug) { + mDebug = debug; + } + + public void startMethodTracing(@NonNull String tracePath) { + if (!mDebug) return; + Debug.startMethodTracing(tracePath); + } + + public void stopMethodTracing() { + if (!mDebug) return; + Debug.stopMethodTracing(); + } + +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/RxNet.java b/library/src/main/java/com/chwl/library/net/rxnet/RxNet.java new file mode 100644 index 0000000..01c08a7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/RxNet.java @@ -0,0 +1,69 @@ +package com.chwl.library.net.rxnet; + +import android.content.Context; + +import com.chwl.library.R; +import com.chwl.library.net.rxnet.manager.RxNetManager; +import com.chwl.library.utils.NullUtils; +import com.chwl.library.utils.ResUtil; + + +/** + *

网络请求入口

+ * + * @author jiahui + * date 2017/12/4 + */ + +public final class RxNet { + + private static RxNet mInstance; + + private static RxNetManager.Builder sBuilder; + + private static Context mContext; + + private RxNet() { + sBuilder = new RxNetManager.Builder(); + sBuilder.setContext(mContext); + } + + public static RxNet get() { + if (mInstance == null) { + synchronized (RxNet.class) { + if (mInstance == null) { + mInstance = new RxNet(); + } + } + } + return mInstance; + } + + public static RxNetManager.Builder init(Context context) { + mContext = context; + get(); + return sBuilder; + } + + public static Context getContext() { + return mContext; + } + + public static T create(Class service) { + checkInstance(); + return sBuilder.getRxNetManager().getRetrofit().create(service); + } + /** + * 刷新 BaseUrl + */ + public static void refreshBaseUrl(String newBaseUrl) { + checkInstance(); + sBuilder.getRxNetManager().refreshBaseUrl(newBaseUrl); + } + + + private static void checkInstance() { + NullUtils.checkNull(mInstance, ResUtil.getString(R.string.net_rxnet_rxnet_01)); + } + +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/callback/CallBack.java b/library/src/main/java/com/chwl/library/net/rxnet/callback/CallBack.java new file mode 100644 index 0000000..15440d1 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/callback/CallBack.java @@ -0,0 +1,24 @@ +package com.chwl.library.net.rxnet.callback; + +/** + *

网络请求回调

+ * + * @author jiahui + * date 2017/12/6 + */ +public interface CallBack { + /** + * 网络请求成功 + * + * @param data 返回数据 + */ + void onSuccess(T data); + + /** + * 获取数据失败回调方法 + * + * @param code 错误码 + * @param error 失败的信息 + */ + void onFail(int code, String error); +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterFactory.java b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterFactory.java new file mode 100644 index 0000000..ea173a8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Square, 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.chwl.library.net.rxnet.converter; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +/** + * A {@linkplain Converter.Factory converter} which uses Gson for JSON. + * + *

Because Gson is so flexible in the types it supports, this converter assumes that it can + * handle all types. If you are mixing JSON serialization with something else (such as protocol + * buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this + * instance} last to allow the other converters a chance to see their types. + */ +public final class GsonConverterFactory extends Converter.Factory { + /** + * Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and + * decoding from JSON (when no charset is specified by a header) will use UTF-8. + */ + public static GsonConverterFactory create() { + return create(new Gson()); + } + + /** + * Create an instance using {@code gson} for conversion. Encoding to JSON and decoding from JSON + * (when no charset is specified by a header) will use UTF-8. + */ + @SuppressWarnings("ConstantConditions") // Guarding public API nullability. + public static GsonConverterFactory create(Gson gson) { + if (gson == null) throw new NullPointerException("gson == null"); + return new GsonConverterFactory(gson); + } + + private final Gson gson; + + private GsonConverterFactory(Gson gson) { + this.gson = gson; + } + + @Override + public Converter responseBodyConverter( + Type type, Annotation[] annotations, Retrofit retrofit) { + TypeAdapter adapter = gson.getAdapter(TypeToken.get(type)); + return new GsonResponseBodyConverter<>(gson, adapter); + } + + @Override + public Converter requestBodyConverter( + Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit) { + TypeAdapter adapter = gson.getAdapter(TypeToken.get(type)); + return new GsonRequestBodyConverter<>(gson, adapter); + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterPlugins.java b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterPlugins.java new file mode 100644 index 0000000..87c92f5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonConverterPlugins.java @@ -0,0 +1,29 @@ +package com.chwl.library.net.rxnet.converter; + + +import androidx.annotation.Nullable; + +import kotlin.jvm.functions.Function1; + +public class GsonConverterPlugins { + + private static Function1 resultHandler; + static volatile boolean lockdown; + + public static void onConvertResult(@Nullable Object object) { + if (resultHandler != null && object != null) { + resultHandler.invoke(object); + } + } + + public static void setResultHandler(Function1 handler) { + if (lockdown) { + throw new IllegalStateException("GsonConverterPlugins can't be changed anymore"); + } + resultHandler = handler; + } + + public static void lockdown() { + lockdown = true; + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonRequestBodyConverter.java b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonRequestBodyConverter.java new file mode 100644 index 0000000..2114245 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonRequestBodyConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 Square, 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.chwl.library.net.rxnet.converter; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonWriter; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import retrofit2.Converter; + +final class GsonRequestBodyConverter implements Converter { + private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final Gson gson; + private final TypeAdapter adapter; + + GsonRequestBodyConverter(Gson gson, TypeAdapter adapter) { + this.gson = gson; + this.adapter = adapter; + } + + @Override + public RequestBody convert(@NotNull T value) throws IOException { + Buffer buffer = new Buffer(); + Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8); + JsonWriter jsonWriter = gson.newJsonWriter(writer); + adapter.write(jsonWriter, value); + jsonWriter.close(); + return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonResponseBodyConverter.java b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonResponseBodyConverter.java new file mode 100644 index 0000000..3cd5f35 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/converter/GsonResponseBodyConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 Square, 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.chwl.library.net.rxnet.converter; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +import java.io.IOException; + +import okhttp3.ResponseBody; +import retrofit2.Converter; + +final class GsonResponseBodyConverter implements Converter { + private final Gson gson; + private final TypeAdapter adapter; + + GsonResponseBodyConverter(Gson gson, TypeAdapter adapter) { + this.gson = gson; + this.adapter = adapter; + } + + @Override + public T convert(ResponseBody value) throws IOException { + JsonReader jsonReader = gson.newJsonReader(value.charStream()); + try { + T result = adapter.read(jsonReader); + if (jsonReader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonIOException("JSON document was not fully consumed."); + } + //hook + GsonConverterPlugins.onConvertResult(result); + + return result; + } finally { + value.close(); + } + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/exception/ApiException.kt b/library/src/main/java/com/chwl/library/net/rxnet/exception/ApiException.kt new file mode 100644 index 0000000..35f7d81 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/exception/ApiException.kt @@ -0,0 +1,20 @@ +package com.chwl.library.net.rxnet.exception + +/** + * Created by qisan 2022/5/26 + * com.qisan.wanandroid.http.exception + */ +class ApiException : RuntimeException { + + private var code: Int? = null + + constructor(throwable: Throwable, code: Int) : super(throwable) { + this.code = code + } + + constructor(message: String) : super(Throwable(message)) + + constructor(message: String,code: Int) : super(Throwable(message)){ + this.code = code + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/net/rxnet/exception/ErrorStatus.kt b/library/src/main/java/com/chwl/library/net/rxnet/exception/ErrorStatus.kt new file mode 100644 index 0000000..dbef2b9 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/exception/ErrorStatus.kt @@ -0,0 +1,37 @@ +package com.chwl.library.net.rxnet.exception + +/** + * Created by qisan 2022/5/26 + * com.qisan.wanandroid.http.exception + */ +object ErrorStatus { + /** + * 响应成功 + */ + const val SUCCESS = 0 + + /** + * Token 过期 + */ + const val TOKEN_INVALID = 401 + + /** + * 未知错误 + */ + const val UNKNOWN_ERROR = 1002 + + /** + * 服务器内部错误 + */ + const val SERVER_ERROR = 1003 + + /** + * 网络连接超时 + */ + const val NETWORK_ERROR = 1004 + + /** + * API解析异常(或者第三方数据结构更改)等其他异常 + */ + const val API_ERROR = 1005 +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/net/rxnet/exception/ExceptionHandle.kt b/library/src/main/java/com/chwl/library/net/rxnet/exception/ExceptionHandle.kt new file mode 100644 index 0000000..6c913ce --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/exception/ExceptionHandle.kt @@ -0,0 +1,58 @@ +package com.chwl.library.net.rxnet.exception + +import android.util.Log +import com.chwl.library.R +import com.chwl.library.utils.ResUtil +import com.google.gson.JsonParseException +import org.json.JSONException +import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.text.ParseException + +/** + * Created by wushaocheng 2023/1/31 + */ +class ExceptionHandle { + + companion object { + private const val TAG = "ExceptionHandle" + var errorMsg = ResUtil.getString(R.string.request_failed_again_later) + + fun handleException(e: Throwable): String { + e.printStackTrace() + when (e) { + is SocketException, is SocketTimeoutException, is HttpException -> { //均视为网络错误 + Log.e(TAG, "網絡連接異常: " + e.message) + errorMsg = ResUtil.getString(R.string.network_abnormality_check_again) + } + is JsonParseException, is JSONException, is ParseException -> { //均视为解析错误 + Log.e(TAG, "數據解析異常: " + e.message) + errorMsg = ResUtil.getString(R.string.data_parsing_exception) + } + is ApiException -> {//服务器返回的错误信息 + errorMsg = e.message.toString() + } + is UnknownHostException -> { + Log.e(TAG, "網絡連接異常: " + e.message) + errorMsg = ResUtil.getString(R.string.network_abnormality_check_again) + } + is IllegalArgumentException -> { + errorMsg = ResUtil.getString(R.string.parameter_error) + } + else -> {//未知错误 + try { + Log.e(TAG, "錯誤: " + e.message) + } catch (e1: Exception) { + Log.e(TAG, "未知錯誤Debug調試 ") + } + errorMsg = e.message.toString() + } + } + return errorMsg + } + + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/net/rxnet/factory/Tls12SocketFactory.java b/library/src/main/java/com/chwl/library/net/rxnet/factory/Tls12SocketFactory.java new file mode 100644 index 0000000..45edfd8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/factory/Tls12SocketFactory.java @@ -0,0 +1,68 @@ +package com.chwl.library.net.rxnet.factory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * Enables TLS v1.2 when creating SSLSockets. + *

+ * For some reason, android supports TLS v1.2 from API 16, but enables it by + * default only from API 20. + * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html + * @see SSLSocketFactory + */ +public class Tls12SocketFactory extends SSLSocketFactory { + private static final String[] TLS_V12_ONLY = {"TLSv1.2"}; + + final SSLSocketFactory delegate; + + public Tls12SocketFactory(SSLSocketFactory base) { + this.delegate = base; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return patch(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return patch(delegate.createSocket(address, port, localAddress, localPort)); + } + + private Socket patch(Socket s) { + if (s instanceof SSLSocket) { + ((SSLSocket) s).setEnabledProtocols(TLS_V12_ONLY); + } + return s; + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/https/HttpsUtils.java b/library/src/main/java/com/chwl/library/net/rxnet/https/HttpsUtils.java new file mode 100644 index 0000000..7b6d970 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/https/HttpsUtils.java @@ -0,0 +1,179 @@ +package com.chwl.library.net.rxnet.https; + + +import com.chwl.library.net.rxnet.utils.RxNetLog; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + *

Https工具类 (签名,自签名信息设置)

+ * + * @author jiahui + * date 2017/12/4 + */ +public class HttpsUtils { + + public static class SSLParams { + public SSLSocketFactory sSLSocketFactory; + public X509TrustManager trustManager; + } + + public static SSLParams getSslSocketFactory(InputStream bksFile, String password, InputStream[] certificates) { + SSLParams sslParams = new SSLParams(); + try { + KeyManager[] keyManagers = prepareKeyManager(bksFile, password); + TrustManager[] trustManagers = prepareTrustManager(certificates); + SSLContext sslContext = SSLContext.getInstance("TLS"); + X509TrustManager trustManager; + if (trustManagers != null) { + trustManager = new MyTrustManager(chooseTrustManager(trustManagers)); + } else { + trustManager = new UnSafeTrustManager(); + } + sslContext.init(keyManagers, new TrustManager[]{trustManager}, null); + sslParams.sSLSocketFactory = sslContext.getSocketFactory(); + sslParams.trustManager = trustManager; + return sslParams; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (KeyManagementException e) { + throw new AssertionError(e); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + private static TrustManager[] prepareTrustManager(InputStream... certificates) { + if (certificates == null || certificates.length <= 0) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + int index = 0; + for (InputStream certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); + try { + if (certificate != null) { + certificate.close(); + } + } catch (IOException e) { + RxNetLog.e(e.getMessage()); + } + } + TrustManagerFactory trustManagerFactory; + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } catch (NoSuchAlgorithmException e) { + RxNetLog.e(e.getMessage()); + } catch (CertificateException e) { + RxNetLog.e(e.getMessage()); + } catch (KeyStoreException e) { + RxNetLog.e(e.getMessage()); + } catch (Exception e) { + RxNetLog.e(e.getMessage()); + } + return null; + } + + private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { + try { + if (bksFile == null || password == null) { + return null; + } + KeyStore clientKeyStore = KeyStore.getInstance("BKS"); + clientKeyStore.load(bksFile, password.toCharArray()); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(clientKeyStore, password.toCharArray()); + return keyManagerFactory.getKeyManagers(); + } catch (KeyStoreException e) { + RxNetLog.e(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + RxNetLog.e(e.getMessage()); + } catch (UnrecoverableKeyException e) { + RxNetLog.e(e.getMessage()); + } catch (CertificateException e) { + RxNetLog.e(e.getMessage()); + } catch (IOException e) { + RxNetLog.e(e.getMessage()); + } catch (Exception e) { + RxNetLog.e(e.getMessage()); + } + return null; + } + + private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { + for (TrustManager trustManager : trustManagers) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + return null; + } + + private static class UnSafeTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + + private static class MyTrustManager implements X509TrustManager { + private X509TrustManager defaultTrustManager; + private X509TrustManager localTrustManager; + + public MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + var4.init((KeyStore) null); + defaultTrustManager = chooseTrustManager(var4.getTrustManagers()); + this.localTrustManager = localTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + defaultTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException ce) { + localTrustManager.checkServerTrusted(chain, authType); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpCacheInterceptor.java b/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpCacheInterceptor.java new file mode 100644 index 0000000..9a43bfa --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpCacheInterceptor.java @@ -0,0 +1,55 @@ +package com.chwl.library.net.rxnet.interceptor; + +import android.content.Context; + +import com.chwl.library.R; +import com.chwl.library.net.rxnet.utils.RxNetLog; +import com.chwl.library.net.rxnet.utils.RxNetWorkUtils; +import com.chwl.library.utils.ResUtil; + +import java.io.IOException; + +import okhttp3.CacheControl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + *

http 缓存拦截器

+ * + * @author jiahui + * date 2017/12/4 + */ +public class HttpCacheInterceptor implements Interceptor { + + private Context mContext; + + public HttpCacheInterceptor(Context context) { + mContext = context; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + if (!RxNetWorkUtils.isAvailable(mContext)) { + request = request.newBuilder() + .cacheControl(CacheControl.FORCE_CACHE) + .build(); + RxNetLog.d(ResUtil.getString(R.string.rxnet_interceptor_httpcacheinterceptor_01)); + } + Response originalResponse = chain.proceed(request); + if (RxNetWorkUtils.isAvailable(mContext)) { + //有网络的时候读取接口里面的配置,在这里进行统一配置 + String cacheControl = request.cacheControl().toString(); + return originalResponse.newBuilder() + .header("Cache-Control", cacheControl) + .removeHeader("Pragma") + .build(); + } else { + return originalResponse.newBuilder() + .header("Cache-Control", "public, only-if-cached, max-stale=2419200") + .removeHeader("Pragma") + .build(); + } + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpLoggingInterceptor.java b/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpLoggingInterceptor.java new file mode 100644 index 0000000..4fae361 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/interceptor/HttpLoggingInterceptor.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2015 Square, 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.chwl.library.net.rxnet.interceptor; + +import static okhttp3.internal.platform.Platform.INFO; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +import okhttp3.Connection; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.http.HttpHeaders; +import okhttp3.internal.platform.Platform; +import okio.Buffer; +import okio.BufferedSource; +import okio.GzipSource; + +/** + * An OkHttp interceptor which logs request and response information. Can be applied as an + * {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain + * OkHttpClient#networkInterceptors() network interceptor}.

The format of the logs created by + * this class should not be considered stable and may change slightly between releases. If you need + * a stable logging format, use your own interceptor. + */ +public final class HttpLoggingInterceptor implements Interceptor { + private static final Charset UTF8 = Charset.forName("UTF-8"); + + public enum Level { + /** No logs. */ + NONE, + /** + * Logs request and response lines. + * + *

Example: + *

{@code
+     * --> POST /greeting http/1.1 (3-byte body)
+     *
+     * <-- 200 OK (22ms, 6-byte body)
+     * }
+ */ + BASIC, + /** + * Logs request and response lines and their respective headers. + * + *

Example: + *

{@code
+     * --> POST /greeting http/1.1
+     * Host: example.com
+     * Content-Type: plain/text
+     * Content-Length: 3
+     * --> END POST
+     *
+     * <-- 200 OK (22ms)
+     * Content-Type: plain/text
+     * Content-Length: 6
+     * <-- END HTTP
+     * }
+ */ + HEADERS, + /** + * Logs request and response lines and their respective headers and bodies (if present). + * + *

Example: + *

{@code
+     * --> POST /greeting http/1.1
+     * Host: example.com
+     * Content-Type: plain/text
+     * Content-Length: 3
+     *
+     * Hi?
+     * --> END POST
+     *
+     * <-- 200 OK (22ms)
+     * Content-Type: plain/text
+     * Content-Length: 6
+     *
+     * Hello!
+     * <-- END HTTP
+     * }
+ */ + BODY + } + + public interface Logger { + void log(String message); + + /** A {@link Logger} defaults output appropriate for the current platform. */ + Logger DEFAULT = message -> Platform.get().log( message, INFO,null); + } + + public HttpLoggingInterceptor() { + this(Logger.DEFAULT); + } + + public HttpLoggingInterceptor(Logger logger) { + this.logger = logger; + } + + private final Logger logger; + + private volatile Set headersToRedact = Collections.emptySet(); + + public void redactHeader(String name) { + Set newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + newHeadersToRedact.addAll(headersToRedact); + newHeadersToRedact.add(name); + headersToRedact = newHeadersToRedact; + } + + private volatile Level level = Level.NONE; + + /** Change the level at which this interceptor logs. */ + public HttpLoggingInterceptor setLevel(Level level) { + if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead."); + this.level = level; + return this; + } + + public Level getLevel() { + return level; + } + + @Override public synchronized Response intercept(Chain chain) throws IOException { + Level level = this.level; + + Request request = chain.request(); + if (level == Level.NONE) { + return chain.proceed(request); + } + + boolean logBody = level == Level.BODY; + boolean logHeaders = logBody || level == Level.HEADERS; + + RequestBody requestBody = request.body(); + boolean hasRequestBody = requestBody != null; + + Connection connection = chain.connection(); + String requestStartMessage = "--> " + + request.method() + + ' ' + request.url() + + (connection != null ? " " + connection.protocol() : ""); + if (!logHeaders && hasRequestBody) { + requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; + } + logger.log(requestStartMessage); + + if (logHeaders) { + if (hasRequestBody) { + // Request body headers are only present when installed as a network interceptor. Force + // them to be included (when available) so there values are known. + if (requestBody.contentType() != null) { + logger.log("Content-Type: " + requestBody.contentType()); + } + if (requestBody.contentLength() != -1) { + logger.log("Content-Length: " + requestBody.contentLength()); + } + } + + Headers headers = request.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + String name = headers.name(i); + // Skip headers from the request body as they are explicitly logged above. + if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) { + logHeader(headers, i); + } + } + + if (!logBody || !hasRequestBody) { + logger.log("--> END " + request.method()); + } else if (bodyHasUnknownEncoding(request.headers())) { + logger.log("--> END " + request.method() + " (encoded body omitted)"); + } else if (requestBody.isDuplex()) { + logger.log("--> END " + request.method() + " (duplex request body omitted)"); + } else { + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + + Charset charset = UTF8; + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + logger.log(""); + if (isPlaintext(buffer)) { + logger.log(buffer.readString(charset)); + logger.log("--> END " + request.method() + + " (" + requestBody.contentLength() + "-byte body)"); + } else { + logger.log("--> END " + request.method() + " (binary " + + requestBody.contentLength() + "-byte body omitted)"); + } + } + } + + long startNs = System.nanoTime(); + Response response; + try { + response = chain.proceed(request); + } catch (Exception e) { + logger.log("<-- HTTP FAILED: " + e); + throw e; + } + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; + logger.log("<-- " + + response.code() + + (response.message().isEmpty() ? "" : ' ' + response.message()) + + ' ' + response.request().url() + + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')'); + + if (logHeaders) { + Headers headers = response.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + logHeader(headers, i); + } + + if (!logBody || !HttpHeaders.hasBody(response)) { + logger.log("<-- END HTTP"); + } else if (bodyHasUnknownEncoding(response.headers())) { + logger.log("<-- END HTTP (encoded body omitted)"); + } else { + BufferedSource source = responseBody.source(); + source.request(Long.MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.getBuffer(); + + Long gzippedLength = null; + if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) { + gzippedLength = buffer.size(); + try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) { + buffer = new Buffer(); + buffer.writeAll(gzippedResponseBody); + } + } + + Charset charset = UTF8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + if (!isPlaintext(buffer)) { + logger.log(""); + logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); + return response; + } + + if (contentLength != 0) { + logger.log(""); + logger.log(buffer.clone().readString(charset)); + } + + if (gzippedLength != null) { + logger.log("<-- END HTTP (" + buffer.size() + "-byte, " + + gzippedLength + "-gzipped-byte body)"); + } else { + logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); + } + } + } + + return response; + } + + private void logHeader(Headers headers, int i) { + String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i); + logger.log(headers.name(i) + ": " + value); + } + + /** + * Returns true if the body in question probably contains human readable text. Uses a small sample + * of code points to detect unicode control characters commonly used in binary file signatures. + */ + static boolean isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < 16; i++) { + if (prefix.exhausted()) { + break; + } + int codePoint = prefix.readUtf8CodePoint(); + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false; + } + } + return true; + } catch (EOFException e) { + return false; // Truncated UTF-8 sequence. + } + } + + private static boolean bodyHasUnknownEncoding(Headers headers) { + String contentEncoding = headers.get("Content-Encoding"); + return contentEncoding != null + && !contentEncoding.equalsIgnoreCase("identity") + && !contentEncoding.equalsIgnoreCase("gzip"); + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/manager/CacheManager.java b/library/src/main/java/com/chwl/library/net/rxnet/manager/CacheManager.java new file mode 100644 index 0000000..34c55da --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/manager/CacheManager.java @@ -0,0 +1,39 @@ +package com.chwl.library.net.rxnet.manager; + +import android.content.Context; + +import com.chwl.library.net.rxnet.interceptor.HttpCacheInterceptor; +import com.chwl.library.net.rxnet.utils.RxFileUtils; + +import java.io.File; + +import okhttp3.Cache; + +/** + *

网络请求缓存管理

+ * + * @author jiahui + * date 2017/12/4 + */ + +final class CacheManager { + /** 默认100M */ + private static final int MAX_CACHE = 100 * 1024 * 1024; + + private Cache mCache; + private HttpCacheInterceptor mHttpCacheInterceptor; + + CacheManager(Context context) { + File cacheFile = new File(RxFileUtils.getCacheDir(context), "net"); + mCache = new Cache(cacheFile, MAX_CACHE); + mHttpCacheInterceptor = new HttpCacheInterceptor(context); + } + + public Cache getCache() { + return mCache; + } + + public HttpCacheInterceptor getHttpCacheInterceptor() { + return mHttpCacheInterceptor; + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/manager/RxNetManager.java b/library/src/main/java/com/chwl/library/net/rxnet/manager/RxNetManager.java new file mode 100644 index 0000000..1a51dc9 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/manager/RxNetManager.java @@ -0,0 +1,222 @@ +package com.chwl.library.net.rxnet.manager; + +import android.content.Context; + +import com.chwl.library.BuildConfig; +import com.chwl.library.net.rxnet.converter.GsonConverterFactory; +import com.chwl.library.net.rxnet.https.HttpsUtils; +import com.chwl.library.net.rxnet.utils.RxNetLog; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.net.ssl.HostnameVerifier; + +import okhttp3.Cache; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; + +/** + *

RxNet 管理类

+ * + * @author jiahui + * date 2017/12/4 + */ + +public final class RxNetManager { + private OkHttpClient mOkHttpClient; + private OkHttpClient.Builder mBuilder; + + private Retrofit mRetrofit; + + RxNetManager(Context context, String baseUrl, Cache cache, int readTimeout, + int writeTimeout, int connectTimeout, List interceptors, + HttpsUtils.SSLParams sslParams, HostnameVerifier hostnameVerifier) { + + mBuilder = new OkHttpClient.Builder(); + + if (RxNetLog.DEBUG) { + //正式环境千万不要加这玩意,为了方便日志查看,拦截器里面加了synchronized关键字,接口请求是串行的 +// HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { +// @Override +// public void log(String message) { +// RxNetLog.d("OKHttp-------%s", message); +// } +// }); + okhttp3.logging.HttpLoggingInterceptor loggingInterceptor = new okhttp3.logging.HttpLoggingInterceptor(); + loggingInterceptor.setLevel(okhttp3.logging.HttpLoggingInterceptor.Level.BODY); + mBuilder.addInterceptor(loggingInterceptor); + } + + for (Interceptor interceptor : interceptors) { + mBuilder.addInterceptor(interceptor); + } + + //mBuilder.readTimeout(readTimeout > 0 ? readTimeout : DEFAULT_READ_TIME_OUT, TimeUnit.MILLISECONDS) + // .writeTimeout(writeTimeout > 0 ? writeTimeout : DEFAULT_WRITE_TIME_OUT, TimeUnit.MILLISECONDS) + // .connectTimeout(connectTimeout > 0 ? connectTimeout : DEFAULT_CONNECT_TIME_OUT, TimeUnit.MILLISECONDS) + // .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)); + // 无代理设置,防止被抓包 + // 指定只要不是 release 包都可以抓包,方便测试进行验证问题 + if (Objects.equals(BuildConfig.BUILD_TYPE, "release")) { + mBuilder.proxySelector(new ProxySelector() { + @Override + public List select(URI uri) { + return Collections.singletonList(Proxy.NO_PROXY); + } + + @Override + public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) { + + } + }); + } + + mOkHttpClient = mBuilder.build(); + Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().create(); + mRetrofit = new Retrofit.Builder() + .client(mOkHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .baseUrl(baseUrl) + .build(); + + } + + public void refreshBaseUrl(String baseUrl) { + Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().create(); + mRetrofit = new Retrofit.Builder() + .client(mOkHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .baseUrl(baseUrl) + .build(); + } + + public Retrofit getRetrofit() { + return mRetrofit; + } + + public static final class Builder { + private String baseUrl; + private Context mContext; + private Cache mCache; + + private int readTimeout; + private int writeTimeout; + private int connectTimeout; + + private List interceptors; + private HttpsUtils.SSLParams sslParams; + private HostnameVerifier hostnameVerifier; + + private RxNetManager mRxNetManager; + + public Builder debug(boolean isDebug) { + RxNetLog.DEBUG = isDebug; + return this; + } + + public Builder setContext(Context context) { + mContext = context; + return this; + } + + public Builder setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Builder setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + public Builder setWriteTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + return this; + } + + public Builder setCache(Cache cache) { + mCache = cache; + return this; + } + + /** + * 添加拦截器 + * + * @param interceptor + */ + public Builder addInterceptors(Interceptor interceptor) { + if (interceptors == null) { + interceptors = new ArrayList<>(); + } + this.interceptors.add(interceptor); + return this; + } + + + /** + * https的全局自签名证书 + * + * @param certificates + * @return + */ + public Builder certificates(InputStream... certificates) { + this.sslParams = HttpsUtils.getSslSocketFactory(null, null, certificates); + return this; + } + + /** + * https双向认证证书 + * + * @param bksFile + * @param password + * @param certificates + * @return + */ + public Builder certificates(InputStream bksFile, String password, InputStream... certificates) { + this.sslParams = HttpsUtils.getSslSocketFactory(bksFile, password, certificates); + return this; + } + + + /** + * https的全局访问规则 + * + * @param hostnameVerifier + * @return + */ + public Builder hostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + + public void build() { + mRxNetManager = new RxNetManager(mContext, baseUrl, mCache, readTimeout, writeTimeout, + connectTimeout, interceptors, sslParams, hostnameVerifier); + } + + public RxNetManager getRxNetManager() { + return mRxNetManager; + } + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/model/HttpHeaders.java b/library/src/main/java/com/chwl/library/net/rxnet/model/HttpHeaders.java new file mode 100644 index 0000000..2ce1a4b --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/model/HttpHeaders.java @@ -0,0 +1,271 @@ +package com.chwl.library.net.rxnet.model; + +import android.os.Build; +import android.text.TextUtils; + +import com.chwl.library.net.rxnet.RxNet; +import com.chwl.library.net.rxnet.utils.RxNetLog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +/** + *

http头部 实体封装

+ * + * @author jiahui + * date 2017/12/7 + */ +public class HttpHeaders { + public static final String FORMAT_HTTP_DATA = "EEE, dd MMM y HH:mm:ss 'GMT'"; + public static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT"); + + public static final String HEAD_KEY_RESPONSE_CODE = "ResponseCode"; + public static final String HEAD_KEY_RESPONSE_MESSAGE = "ResponseMessage"; + public static final String HEAD_KEY_ACCEPT = "Accept"; + public static final String HEAD_KEY_ACCEPT_ENCODING = "Accept-Encoding"; + public static final String HEAD_VALUE_ACCEPT_ENCODING = "gzip, deflate"; + public static final String HEAD_KEY_ACCEPT_LANGUAGE = "Accept-Language"; + public static final String HEAD_KEY_CONTENT_TYPE = "Content-Type"; + public static final String HEAD_KEY_CONTENT_LENGTH = "Content-Length"; + public static final String HEAD_KEY_CONTENT_ENCODING = "Content-Encoding"; + public static final String HEAD_KEY_CONTENT_DISPOSITION = "Content-Disposition"; + public static final String HEAD_KEY_CONTENT_RANGE = "Content-Range"; + public static final String HEAD_KEY_CACHE_CONTROL = "Cache-Control"; + public static final String HEAD_KEY_CONNECTION = "Connection"; + public static final String HEAD_VALUE_CONNECTION_KEEP_ALIVE = "keep-alive"; + public static final String HEAD_VALUE_CONNECTION_CLOSE = "close"; + public static final String HEAD_KEY_DATE = "Date"; + public static final String HEAD_KEY_EXPIRES = "Expires"; + public static final String HEAD_KEY_E_TAG = "ETag"; + public static final String HEAD_KEY_PRAGMA = "Pragma"; + public static final String HEAD_KEY_IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String HEAD_KEY_IF_NONE_MATCH = "If-None-Match"; + public static final String HEAD_KEY_LAST_MODIFIED = "Last-Modified"; + public static final String HEAD_KEY_LOCATION = "Location"; + public static final String HEAD_KEY_USER_AGENT = "User-Agent"; + public static final String HEAD_KEY_COOKIE = "Cookie"; + public static final String HEAD_KEY_COOKIE2 = "Cookie2"; + public static final String HEAD_KEY_SET_COOKIE = "Set-Cookie"; + public static final String HEAD_KEY_SET_COOKIE2 = "Set-Cookie2"; + + public LinkedHashMap headersMap; + private static String acceptLanguage; + private static String userAgent; + + private void init() { + headersMap = new LinkedHashMap<>(); + } + + public HttpHeaders() { + init(); + } + + public HttpHeaders(String key, String value) { + init(); + put(key, value); + } + + public void put(String key, String value) { + if (key != null && value != null) { + headersMap.remove(key); + headersMap.put(key, value); + } + } + + public void put(HttpHeaders headers) { + if (headers != null) { + if (headers.headersMap != null && !headers.headersMap.isEmpty()) { + Set> set = headers.headersMap.entrySet(); + for (Map.Entry map : set) { + headersMap.remove(map.getKey()); + headersMap.put(map.getKey(), map.getValue()); + } + } + + } + } + + public boolean isEmpty() { + return headersMap.isEmpty(); + } + + public String get(String key) { + return headersMap.get(key); + } + + public String remove(String key) { + return headersMap.remove(key); + } + + public void clear() { + headersMap.clear(); + } + + public Set getNames() { + return headersMap.keySet(); + } + + public final String toJSONString() { + JSONObject jsonObject = new JSONObject(); + try { + for (Map.Entry entry : headersMap.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + } catch (JSONException e) { + RxNetLog.e(e.getMessage()); + } + return jsonObject.toString(); + } + + public static long getDate(String gmtTime) { + try { + return parseGMTToMillis(gmtTime); + } catch (ParseException e) { + return 0; + } + } + + public static String getDate(long milliseconds) { + return formatMillisToGMT(milliseconds); + } + + public static long getExpiration(String expiresTime) { + try { + return parseGMTToMillis(expiresTime); + } catch (ParseException e) { + return -1; + } + } + + public static long getLastModified(String lastModified) { + try { + return parseGMTToMillis(lastModified); + } catch (ParseException e) { + return 0; + } + } + + public static String getCacheControl(String cacheControl, String pragma) { + // first http1.1, second http1.0 + if (cacheControl != null) { + return cacheControl; + } else if (pragma != null) { + return pragma; + } else { + return null; + } + } + + public static void setAcceptLanguage(String language) { + acceptLanguage = language; + } + + public static String getAcceptLanguage() { + if (TextUtils.isEmpty(acceptLanguage)) { + Locale locale = Locale.getDefault(); + String language = locale.getLanguage(); + String country = locale.getCountry(); + StringBuilder acceptLanguageBuilder = new StringBuilder(language); + if (!TextUtils.isEmpty(country)) { + acceptLanguageBuilder.append('-').append(country).append(',').append(language).append(";q=0.8"); + } + acceptLanguage = acceptLanguageBuilder.toString(); + return acceptLanguage; + } + return acceptLanguage; + } + + public static void setUserAgent(String agent) { + userAgent = agent; + } + + + public static String getUserAgent() { + if (TextUtils.isEmpty(userAgent)) { + String webUserAgent = null; + try { + Class sysResCls = Class.forName("com.android.internal.R$string"); + Field webUserAgentField = sysResCls.getDeclaredField("web_user_agent"); + Integer resId = (Integer) webUserAgentField.get(null); + webUserAgent = RxNet.getContext().getString(resId); + } catch (Exception e) { + // We have nothing to do + } + if (TextUtils.isEmpty(webUserAgent)) { + webUserAgent = "Mozilla/5.0 (Linux; U; Android %s) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 %sSafari/533.1"; + } + + Locale locale = Locale.getDefault(); + StringBuffer buffer = new StringBuffer(); + // Add version + final String version = Build.VERSION.RELEASE; + if (version.length() > 0) { + buffer.append(version); + } else { + // default to "1.0" + buffer.append("1.0"); + } + buffer.append("; "); + final String language = locale.getLanguage(); + if (language != null) { + buffer.append(language.toLowerCase(locale)); + final String country = locale.getCountry(); + if (!TextUtils.isEmpty(country)) { + buffer.append("-"); + buffer.append(country.toLowerCase(locale)); + } + } else { + // default to "en" + buffer.append("en"); + } + // add the model for the release build + if ("REL".equals(Build.VERSION.CODENAME)) { + final String model = Build.MODEL; + if (model.length() > 0) { + buffer.append("; "); + buffer.append(model); + } + } + final String id = Build.ID; + if (id.length() > 0) { + buffer.append(" Build/"); + buffer.append(id); + } + userAgent = String.format(webUserAgent, buffer, "Mobile "); + return userAgent; + } + return userAgent; + } + + public static long parseGMTToMillis(String gmtTime) throws ParseException { + if (TextUtils.isEmpty(gmtTime)) { + return 0; + } + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_HTTP_DATA, Locale.US); + formatter.setTimeZone(GMT_TIME_ZONE); + Date date = formatter.parse(gmtTime); + return date.getTime(); + } + + public static String formatMillisToGMT(long milliseconds) { + Date date = new Date(milliseconds); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(FORMAT_HTTP_DATA, Locale.US); + simpleDateFormat.setTimeZone(GMT_TIME_ZONE); + return simpleDateFormat.format(date); + } + + @Override + public String toString() { + return "HttpHeaders{" + "headersMap=" + headersMap + '}'; + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/utils/NetworkLatencyChecker.kt b/library/src/main/java/com/chwl/library/net/rxnet/utils/NetworkLatencyChecker.kt new file mode 100644 index 0000000..1f693f6 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/utils/NetworkLatencyChecker.kt @@ -0,0 +1,137 @@ +package com.chwl.library.net.rxnet.utils + + +import android.annotation.SuppressLint +import androidx.lifecycle.MutableLiveData +import com.chwl.library.common.util.doLog +import com.chwl.library.common.util.isVerify +import com.chwl.library.utils.TimeUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.InetSocketAddress +import java.net.Socket + +object NetworkLatencyChecker { + + + /** + * 检查域名列表并返回延迟最低的域名 + * @param domains 要检查的域名列表 + * @param scope 协程作用域(推荐使用 ViewModel 的 viewModelScope) + * @param port 要测试的端口(默认 HTTPS 443 端口) + * @param timeout 连接超时时间(毫秒) + * @param resultCallback 结果回调(主线程执行) + */ + @SuppressLint("NewApi") + fun findLowestLatencyDomain( + domains: List, + scope: CoroutineScope, + port: Int = 443, + timeout: Int = 5000, + resultCallback: (String?) -> Unit + ) { + scope.launch { + val deferredResults = domains.map { domain -> + async(Dispatchers.IO) { + try { + val socket = Socket() + val startTime = System.currentTimeMillis() + socket.connect(InetSocketAddress(domain, port), timeout) + val latency = System.currentTimeMillis() - startTime + socket.close() + domain to latency + } catch (e: Exception) { + null + } + } + } + + val validResults = deferredResults.awaitAll() + .filterNotNull() + .filter { it.second > 0 } // 过滤无效结果 + + val bestDomain = validResults.minByOrNull { it.second }?.first + + // 切回主线程返回结果 + withContext(Dispatchers.Main) { + resultCallback(bestDomain) + } + } + } + + fun use(domains: List, + port: Int = 443, + timeout: Int = 1000, + resultCallback: (String?) -> Unit) { + " 域名 ping 启动---->".doLog() + + GlobalScope.launch { + val deferredResults = domains.map { domain -> + async(Dispatchers.IO) { + try { + val socket = Socket() + val startTime = System.currentTimeMillis() + socket.connect(InetSocketAddress(domain, port), timeout) + val currentTimeMillis = System.currentTimeMillis() + val latency = currentTimeMillis - startTime + " 域名 ping 成功----> $domain startTime= ${TimeUtils.getTimeStringFromMillis(startTime)} currentTimeMillis = ${TimeUtils.getTimeStringFromMillis(currentTimeMillis)} latency=$latency".doLog() + socket.close() + domain to latency + } catch (e: Exception) { + " 域名 ping 失败报错----> $domain 报错 error = ${e.message}".doLog() + null + } + } + } + + + val validResults = deferredResults.awaitAll() + .filterNotNull() + .filter { it.second > 0 } // 过滤无效结果 + + val bestDomain = validResults.minByOrNull { it.second }?.first + " 域名 ping 结果展示----> 耗时最短 $bestDomain".doLog() + + var find = "" + try { + find = validResults.find { it.first.equals("api.molistars.com") }?.first?.toString()?:"" + if (find.isVerify()) { + " 域名 ping 结果展示----> 有效域名包含 $find".doLog() + } + } catch (e: Exception) { + } + + // 切回主线程返回结果 + withContext(Dispatchers.Main) { + if (find.isVerify()) { + " 域名 ping 结果展示----> 返回 find域名".doLog() + resultCallback(find) + } else { + if (bestDomain.isVerify()) { + " 域名 ping 结果展示----> 返回 耗时最短bestDomain域名".doLog() + resultCallback(bestDomain) + } else { + " 域名 ping 结果展示----> 异常返回null".doLog() + resultCallback(null) + } + } + } + + " 域名 ping 结束---->".doLog() + } + } + + /** + * 域名切换完毕, 通知监听者 执行后续逻辑 + */ + fun checkFinishes(){ + mCheckStatusData.value = true + } + + var mCheckStatusData = MutableLiveData(false) +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/net/rxnet/utils/RxFileUtils.java b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxFileUtils.java new file mode 100644 index 0000000..7520313 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxFileUtils.java @@ -0,0 +1,51 @@ +package com.chwl.library.net.rxnet.utils; + +import android.content.Context; +import android.os.Environment; + +import java.io.File; + +/** + *

网络文件工具类

+ * + * @author jiahui + * date 2017/12/4 + */ + +public class RxFileUtils { + + /** + * 获取缓存目录,有限获取/sdcard/Android/data/package_name/cache,失败才获取/data/data/com.android.framework/cache + * + * @param context + * @return 返回缓存路径 + */ + public static String getCacheDir(Context context) { + // /data/data/com.android.framework/cache + File cacheDir = context.getCacheDir(); + // /sdcard/Android/data/package_name/cache + File externalCacheDir = context.getExternalCacheDir(); + String cacheDirStr; + if (externalCacheDir == null) { + cacheDirStr = cacheDir.getAbsolutePath(); + } else { + cacheDirStr = checkSdCard() ? externalCacheDir.getAbsolutePath() : cacheDir.getAbsolutePath(); + } + return cacheDirStr; + } + + public static boolean checkSdCard() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + /** + * 判断文件是否存在 + * + * @param file 文件 + * @return {@code true}: 存在
{@code false}: 不存在 + */ + public static boolean isFileExists(File file) { + return file != null && file.exists(); + } + +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetLog.java b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetLog.java new file mode 100644 index 0000000..10e7c13 --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetLog.java @@ -0,0 +1,38 @@ +package com.chwl.library.net.rxnet.utils; + +import android.util.Log; + +/** + *

网络日志

+ * + * @author jiahui + * date 2017/12/4 + */ +public class RxNetLog { + public static final String TAG = "RxNet_LOG"; + public static boolean DEBUG = false; + + public static void i(String format, Object... args) { + if (DEBUG) { + Log.i(TAG, String.format(format, args)); + } + } + + public static void d(String format, Object... args) { + if (DEBUG) { + Log.d(TAG, String.format(format, args)); + } + } + + public static void w(String format, Object... args) { + if (DEBUG) { + Log.w(TAG, String.format(format, args)); + } + } + + public static void e(String format, Object... args) { + if (DEBUG) { + Log.e(TAG, String.format(format, args)); + } + } +} diff --git a/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetWorkUtils.java b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetWorkUtils.java new file mode 100644 index 0000000..9e98f4b --- /dev/null +++ b/library/src/main/java/com/chwl/library/net/rxnet/utils/RxNetWorkUtils.java @@ -0,0 +1,38 @@ +package com.chwl.library.net.rxnet.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import com.chwl.library.utils.config.BasicConfig; + +/** + *

网络判断工具类

+ * + * @author jiahui + * date 2017/12/4 + */ +public class RxNetWorkUtils { + /** + * 判断网络是否可用 + *

需添加权限 {@code }

+ * + * @param context 上下文 + * @return {@code true}: 可用
{@code false}: 不可用 + */ + public static boolean isAvailable(Context context) { + NetworkInfo info = getActiveNetworkInfo(BasicConfig.INSTANCE.getAppContext()); + return info != null && info.isAvailable(); + } + + /** + * 获取活动网络信息 + * + * @param context 上下文 + * @return NetworkInfo + */ + public static NetworkInfo getActiveNetworkInfo(Context context) { + ConnectivityManager cm = (ConnectivityManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getActiveNetworkInfo(); + } +} diff --git a/library/src/main/java/com/chwl/library/record/AuditRecorderConfiguration.java b/library/src/main/java/com/chwl/library/record/AuditRecorderConfiguration.java new file mode 100644 index 0000000..0e41af7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/record/AuditRecorderConfiguration.java @@ -0,0 +1,139 @@ +package com.chwl.library.record; + +import android.media.AudioFormat; +import android.media.MediaRecorder; +import android.os.Handler; + +public class AuditRecorderConfiguration { + + public static final int[] SAMPLE_RATES = {44100, 22050, 11025, 8000}; + public static final boolean RECORDING_UNCOMPRESSED = true; + public static final boolean RECORDING_COMPRESSED = false; + + private ExtAudioRecorder.RecorderListener listener; + private boolean uncompressed; + private int timerInterval; + private int rate; + private int source; + private int channelConfig; + private int format; + private Handler handler; + + /** + * 创建一个默认的配置
+ *
    + *
  • uncompressed = false
  • + *
  • timerInterval = 120
  • + *
  • rate = 8000
  • + *
  • source = {@link MediaRecorder.AudioSource#MIC}
  • + *
  • channelConfig = {@link AudioFormat#CHANNEL_CONFIGURATION_MONO}
  • + *
  • format = {@link AudioFormat#ENCODING_PCM_16BIT}
  • + *
+ * + */ + public static AuditRecorderConfiguration createDefaule(){ + return new Builder().builder(); + } + + + public ExtAudioRecorder.RecorderListener getRecorderListener(){ + return listener; + } + + public boolean isUncompressed(){ + return uncompressed; + } + + public int getTimerInterval(){ + return timerInterval; + } + + public int getRate(){ + return rate; + } + + + public int getSource(){ + return source; + } + + public int getFormat(){ + return format; + } + + public Handler getHandler(){ + return handler; + } + + public int getChannelConfig(){ + return channelConfig; + } + + private AuditRecorderConfiguration(Builder builder){ + this.listener = builder.listener; + this.uncompressed = builder.uncompressed; + this.timerInterval = builder.timerInterval; + this.rate = builder.rate; + this.source = builder.source; + this.format = builder.format; + this.handler = builder.handler; + this.channelConfig = builder.channelConfig; + } + + public static class Builder{ + private ExtAudioRecorder.RecorderListener listener; + private boolean uncompressed; + private int timerInterval = 120; + private int rate = SAMPLE_RATES[3]; + private int source = MediaRecorder.AudioSource.MIC; + private int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO; + private int format = AudioFormat.ENCODING_PCM_16BIT; + private Handler handler; + + /** 声道设置 */ + public Builder getChannelConfig(int channelConfig){ + this.channelConfig = channelConfig; + return this; + } + /** 录音失败的监听 */ + public Builder recorderListener(ExtAudioRecorder.RecorderListener listener){ + this.listener = listener; + return this; + } + /** 是否压缩录音 */ + public Builder uncompressed(boolean uncompressed){ + this.uncompressed = uncompressed; + return this; + } + /** 周期的时间间隔 */ + public Builder timerInterval(int timeInterval){ + timerInterval = timeInterval; + return this; + } + /** 采样率 */ + public Builder rate(int rate){ + this.rate = rate; + return this; + } + /** 音频源 */ + public Builder source(int source){ + this.source = source; + return this; + } + /** 编码制式和采样大小 */ + public Builder format(int format){ + this.format = format; + return this; + } + /** 返回what是振幅值 1-13 */ + public Builder handler(Handler handler){ + this.handler = handler; + return this; + } + + public AuditRecorderConfiguration builder(){ + return new AuditRecorderConfiguration(this); + } + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/record/ExtAudioRecorder.java b/library/src/main/java/com/chwl/library/record/ExtAudioRecorder.java new file mode 100644 index 0000000..bbc8e4d --- /dev/null +++ b/library/src/main/java/com/chwl/library/record/ExtAudioRecorder.java @@ -0,0 +1,514 @@ +package com.chwl.library.record; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.os.Message; +import android.os.SystemClock; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Date; + +public class ExtAudioRecorder { + + private AuditRecorderConfiguration configuration; + + public interface RecorderListener { + void recordFailed(FailRecorder failRecorder); + } + + public ExtAudioRecorder(AuditRecorderConfiguration configuration) { + this.configuration = configuration; + + if (configuration.isUncompressed()) { + init(configuration.isUncompressed(), + configuration.getSource(), + configuration.getRate(), + configuration.getChannelConfig(), + configuration.getFormat()); + } else { + int i = 0; + do { + init(configuration.isUncompressed(), + configuration.getSource(), + AuditRecorderConfiguration.SAMPLE_RATES[i], + configuration.getChannelConfig(), + configuration.getFormat()); + + } + while ((++i < AuditRecorderConfiguration.SAMPLE_RATES.length) & !(getState() == ExtAudioRecorder.State.INITIALIZING)); + } + } + + /** + * 录音的状态 + */ + public enum State { + /** + * 录音初始化 + */ + INITIALIZING, + /** + * 已准备好录音 + */ + READY, + /** + * 录音中 + */ + RECORDING, + /** + * 录音生了错误 + */ + ERROR, + /** + * 停止录音 + */ + STOPPED + } + + // 不压缩将使用这个进行录音 + private AudioRecord audioRecorder = null; + + // 压缩将使用这进行录音 + private MediaRecorder mediaRecorder = null; + + // 当前的振幅 (只有在未压缩的模式下) + private int cAmplitude = 0; + + // 录音状态 + private State state; + + // 文件 (只有在未压缩的模式下) + private RandomAccessFile randomAccessWriter; + + private int bufferSize; + + // 录音 通知周期(只有在未压缩的模式下) + private int framePeriod; + // 输出的字节(只有在未压缩的模式下) + private byte[] buffer; + + private short samples; + private short channels; + + // 写入头文件的字节数(只有在未压缩的模式下) + // after stop() is called, this size is written to the header/data chunk in + // the wave file + private int payloadSize; + //录音的开始时间 + private long startTime; + + private String filePath; + + /** + * 返回录音的状态 + * + * @return 录音的状态 + */ + public State getState() { + return state; + } + + /* + * + * Method used for recording. + */ + private AudioRecord.OnRecordPositionUpdateListener updateListener = new AudioRecord.OnRecordPositionUpdateListener() { + public void onPeriodicNotification(AudioRecord recorder) { + audioRecorder.read(buffer, 0, buffer.length); // Fill buffer + try { + randomAccessWriter.write(buffer); // Write buffer to file + payloadSize += buffer.length; + if (samples == 16) { + for (int i = 0; i < buffer.length / 2; i++) { // 16bit sample size + short curSample = getShort(buffer[i * 2], buffer[i * 2 + 1]); + if (curSample > cAmplitude) { // Check amplitude + cAmplitude = curSample; + } + } + } else { // 8bit sample size + for (int i = 0; i < buffer.length; i++) { + if (buffer[i] > cAmplitude) { // Check amplitude + cAmplitude = buffer[i]; + } + } + } + } catch (IOException e) { + e.printStackTrace(); + Log.e(ExtAudioRecorder.class.getName(), "Error occured in updateListener, recording is aborted"); + //stop(); + } + } + + public void onMarkerReached(AudioRecord recorder) { + // NOT USED + } + }; + + /** + * 默认的构造方法,如果压缩录音,剩下的参数可以为0.这个方法不会抛出异常,但是会设置状态为 {@link State#ERROR} + * + * @param uncompressed 是否压缩录音 true不压缩,false压缩 + * @param audioSource 音频源:指的是从哪里采集音频。通过 {@link AudioRecord} 的一些常量去设置 + * @param sampleRate 采样率:音频的采样频率,每秒钟能够采样的次数,采样率越高,音质越高。给出的实例是44100、22050、11025但不限于这几个参数。 + * 例如要采集低质量的音频就可以使用4000、8000等低采样率。 + * @param channelConfig 声道设置:Android支持双声道立体声和单声道。MONO单声道,STEREO立体声 + * @param audioFormat 编码制式和采样大小:采集来的数据当然使用PCM编码(脉冲代码调制编码,即PCM编码。PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。) + * android支持的采样大小16bit 或者8bit。当然采样大小越大,那么信息量越多,音质也越高,现在主流的采样大小都是16bit,在低质量的语音传输的时候8bit足够了。 + */ + public void init(boolean uncompressed, int audioSource, int sampleRate, int channelConfig, int audioFormat) { + try { + if (uncompressed) { // RECORDING_UNCOMPRESSED + if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { + samples = 16; + } else { + samples = 8; + } + + if (channelConfig == AudioFormat.CHANNEL_CONFIGURATION_MONO) { + channels = 1; + } else { + channels = 2; + } + + framePeriod = sampleRate * configuration.getTimerInterval() / 1000; + bufferSize = framePeriod * 2 * samples * channels / 8; + if (bufferSize < AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)) { + // Check to make sure + // buffer size is not + // smaller than the + // smallest allowed one + bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); + // Set frame period and timer interval accordingly + framePeriod = bufferSize / (2 * samples * channels / 8); + Log.w(ExtAudioRecorder.class.getName(), "Increasing buffer size to " + Integer.toString(bufferSize)); + } + + audioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSize); + + if (audioRecorder.getState() != AudioRecord.STATE_INITIALIZED) + throw new Exception("AudioRecord initialization failed"); + audioRecorder.setRecordPositionUpdateListener(updateListener); + audioRecorder.setPositionNotificationPeriod(framePeriod); + } else { // RECORDING_COMPRESSED + mediaRecorder = new MediaRecorder(); + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + } + cAmplitude = 0; + filePath = null; + state = State.INITIALIZING; + } catch (Exception e) { + fireFailEvent(FailRecorder.FailType.NO_PERMISSION, e); + if (e.getMessage() != null) { + Log.e(ExtAudioRecorder.class.getName(), e.getMessage()); + } else { + Log.e(ExtAudioRecorder.class.getName(), "Unknown error occured while initializing recording"); + } + state = State.ERROR; + } + } + + /** + * 设置输出的文件路径 + * + * @param argPath 文件路径 + */ + public void setOutputFile(String argPath) { + try { + if (state == State.INITIALIZING) { + filePath = argPath; + if (!configuration.isUncompressed()) { + mediaRecorder.setOutputFile(filePath); + } + } + } catch (Exception e) { + if (e.getMessage() != null) { + Log.e(ExtAudioRecorder.class.getName(), e.getMessage()); + } else { + Log.e(ExtAudioRecorder.class.getName(), + "Unknown error occured while setting output path"); + } + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, e); + } + } + + /** + * Returns the largest amplitude sampled since the last call to this method. + * + * @return returns the largest amplitude since the last call, or 0 when not + * in recording state. + */ + public int getMaxAmplitude() { + if (state == State.RECORDING) { + if (configuration.isUncompressed()) { + int result = cAmplitude; + cAmplitude = 0; + return result; + } else { + try { + return mediaRecorder.getMaxAmplitude(); + } catch (IllegalStateException e) { + return 0; + } + } + } else { + return 0; + } + } + + /** + * 准备录音的录音机, 如果 state 不是 {@link State#INITIALIZING} 或文件路径为null + * 将设置 state 为 {@link State#ERROR}。如果发生异常不会抛出,而是设置 state 为 + * {@link State#ERROR} + */ + public void prepare() { + try { + if (state == State.INITIALIZING) { + if (configuration.isUncompressed()) { + if ((audioRecorder.getState() == AudioRecord.STATE_INITIALIZED) & (filePath != null)) { + // 写文件头 + randomAccessWriter = new RandomAccessFile(filePath, "rw"); + //设置文件长度为0,为了防止这个file以存在 + randomAccessWriter.setLength(0); + randomAccessWriter.writeBytes("RIFF"); + //不知道文件最后的大小,所以设置0 + randomAccessWriter.writeInt(0); + randomAccessWriter.writeBytes("WAVE"); + randomAccessWriter.writeBytes("fmt "); + // Sub-chunk + // size, + // 16 + // for + // PCM + randomAccessWriter.writeInt(Integer.reverseBytes(16)); + // AudioFormat, 1 为 PCM + randomAccessWriter.writeShort(Short.reverseBytes((short) 1)); + // 数字为声道, 1 为 mono, 2 为 stereo + randomAccessWriter.writeShort(Short.reverseBytes(channels)); + // 采样率 + randomAccessWriter.writeInt(Integer.reverseBytes(configuration.getRate())); + // 采样率, SampleRate*NumberOfChannels*BitsPerSample/8 + randomAccessWriter.writeInt(Integer.reverseBytes(configuration.getRate() * samples * channels / 8)); + randomAccessWriter.writeShort(Short.reverseBytes((short) (channels * samples / 8))); + // Block + // align, + // NumberOfChannels*BitsPerSample/8 + randomAccessWriter.writeShort(Short.reverseBytes(samples)); // Bits per sample + randomAccessWriter.writeBytes("data"); + randomAccessWriter.writeInt(0); // Data chunk size not + // known yet, write 0 + + buffer = new byte[framePeriod * samples / 8 * channels]; + state = State.READY; + } else { + Log.e(ExtAudioRecorder.class.getName(), + "prepare() method called on uninitialized recorder"); + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, null); + } + } else { + mediaRecorder.prepare(); + state = State.READY; + } + } else { + Log.e(ExtAudioRecorder.class.getName(), "prepare() method called on illegal state"); + release(); + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, null); + } + } catch (Exception e) { + if (e.getMessage() != null) { + Log.e(ExtAudioRecorder.class.getName(), e.getMessage()); + } else { + Log.e(ExtAudioRecorder.class.getName(), "Unknown error occured in prepare()"); + } + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, e); + } + } + + /** + * 释放与这个类相关的资源,和移除不必要的文件,在必要的时候 + */ + public void release() { + if (state == State.RECORDING) { + stop(); + } else { + if ((state == State.READY) & (configuration.isUncompressed())) { + try { + randomAccessWriter.close(); // 删除准备文件 + } catch (IOException e) { + Log.e(ExtAudioRecorder.class.getName(), "I/O exception occured while closing output file"); + } + (new File(filePath)).delete(); + } + } + + if (configuration.isUncompressed()) { + if (audioRecorder != null) { + audioRecorder.release(); + } + } else { + if (mediaRecorder != null) { + mediaRecorder.release(); + } + } + } + + public void discardRecording() { + stop(); + + File file = new File(filePath); + if (file.exists() && !file.isDirectory()) { + file.delete(); + } + } + + /** + * 重置录音,并设置 state 为 {@link State#INITIALIZING},如果当前状态为 {@link State#RECORDING},将会停止录音。 + * 这个方法不会抛出异常,但是会设置状态为 {@link State#ERROR} + */ + public void reset() { + try { + if (state != State.ERROR) { + release(); + filePath = null; // Reset file path + cAmplitude = 0; // Reset amplitude + if (configuration.isUncompressed()) { + audioRecorder = new AudioRecord(configuration.getSource(), configuration.getRate(), + channels + 1, configuration.getFormat(), bufferSize); + audioRecorder.setRecordPositionUpdateListener(updateListener); + audioRecorder.setPositionNotificationPeriod(framePeriod); + } else { + mediaRecorder = new MediaRecorder(); + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder + .setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mediaRecorder + .setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + } + state = State.INITIALIZING; + } + } catch (Exception e) { + Log.e(ExtAudioRecorder.class.getName(), e.getMessage()); + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, e); + } + } + + /** + * 开始录音,并设置 state 为 {@link State#RECORDING}。在调用这个方法前必须调用 {@link ExtAudioRecorder#prepare()} 方法 + */ + public void start() { + if (state == State.READY) { + if (configuration.isUncompressed()) { + payloadSize = 0; + audioRecorder.startRecording(); + audioRecorder.read(buffer, 0, buffer.length); + } else { + mediaRecorder.start(); + } + state = State.RECORDING; + this.startTime = (new Date()).getTime(); + startGetMaxAmplitudeThread(); + } else { + Log.e(ExtAudioRecorder.class.getName(), "start() called on illegal state"); + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, null); + } + } + + /** + * 停止录音,并设置 state 为 {@link State#STOPPED}。如果要继续使用,则需要调用 {@link #reset()} 方法 + * + * @return 录音的时间 + */ + public int stop() { + if (state == State.RECORDING) { + if (configuration.isUncompressed()) { + audioRecorder.stop(); + + try { + randomAccessWriter.seek(4); // Write size to RIFF header + randomAccessWriter.writeInt(Integer.reverseBytes(36 + payloadSize)); + + randomAccessWriter.seek(40); // Write size to Subchunk2Size + // field + randomAccessWriter.writeInt(Integer.reverseBytes(payloadSize)); + + randomAccessWriter.close(); + } catch (IOException e) { + Log.e(ExtAudioRecorder.class.getName(), + "I/O exception occured while closing output file"); + state = State.ERROR; + } + } else { + try{ + mediaRecorder.stop(); + } catch (Exception e){} + } + state = State.STOPPED; + + File file = new File(filePath); + if (file.exists() && file.isFile()) { + if (file.length() == 0L) { + file.delete(); + return 0; + } else { + int time = (int) ((new Date()).getTime() - this.startTime) / 1000; + return time; + } + } else { + return 0; + } + } else { + Log.e(ExtAudioRecorder.class.getName(), "stop() called on illegal state"); + state = State.ERROR; + fireFailEvent(FailRecorder.FailType.UNKNOWN, null); + return 0; + } + } + + private void startGetMaxAmplitudeThread() { + if (configuration.getHandler() != null) { + new Thread(new Runnable() { + public void run() { + while (true) { + if (state == State.RECORDING) { + Message var1 = new Message(); + var1.what = getMaxAmplitude() * 13 / 32767; + configuration.getHandler().sendMessage(var1); + SystemClock.sleep(100L); + continue; + } + return; + } + } + }).start(); + } + } + /** Converts a byte[2] to a short, in LITTLE_ENDIAN format */ + private short getShort(byte argB1, byte argB2) { + return (short) (argB1 | (argB2 << 8)); + } + + private void fireFailEvent(final FailRecorder.FailType failType, final Throwable failCause) { + + if (configuration.getRecorderListener() != null) { + Runnable r = new Runnable() { + @Override + public void run() { + configuration.getRecorderListener().recordFailed(new FailRecorder(failType, failCause)); + } + }; + r.run(); + } + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/record/FailRecorder.java b/library/src/main/java/com/chwl/library/record/FailRecorder.java new file mode 100644 index 0000000..cd2a15d --- /dev/null +++ b/library/src/main/java/com/chwl/library/record/FailRecorder.java @@ -0,0 +1,27 @@ +package com.chwl.library.record; + +public class FailRecorder { + private final FailType type; + + private final Throwable cause; + + public FailRecorder(FailType type, Throwable cause) { + this.type = type; + this.cause = cause; + } + + public FailType getType() { + return type; + } + + public Throwable getCause() { + return cause; + } + + public enum FailType { + /** 没有权限 */ + NO_PERMISSION, + /** 位置异常 */ + UNKNOWN + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/record/SimpleRecordFailed.java b/library/src/main/java/com/chwl/library/record/SimpleRecordFailed.java new file mode 100644 index 0000000..4047d54 --- /dev/null +++ b/library/src/main/java/com/chwl/library/record/SimpleRecordFailed.java @@ -0,0 +1,8 @@ +package com.chwl.library.record; + +public class SimpleRecordFailed implements ExtAudioRecorder.RecorderListener{ + @Override + public void recordFailed(FailRecorder failRecorder) { + + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/rx/RxRetryWithDelay.java b/library/src/main/java/com/chwl/library/rx/RxRetryWithDelay.java new file mode 100644 index 0000000..f9471d5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/rx/RxRetryWithDelay.java @@ -0,0 +1,46 @@ +package com.chwl.library.rx; + +import android.util.Log; + +import org.reactivestreams.Publisher; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Flowable; +import io.reactivex.functions.Function; + +/** + * Created by MadisonRong on 13/04/2018. + */ + +public class RxRetryWithDelay implements Function, Publisher> { + + private static final String TAG = "RxRetryWithDelay"; + + private final int maxRetries; + private final int retryDelayMillis; + private int retryCount; + + public RxRetryWithDelay(final int maxRetries, final int retryDelayMillis) { + this.maxRetries = maxRetries; + this.retryDelayMillis = retryDelayMillis; + this.retryCount = 0; + } + + @Override + public Publisher apply(Flowable throwableFlowable) throws Exception { + return throwableFlowable.flatMap(new Function>() { + @Override + public Publisher apply(Throwable throwable) throws Exception { + Log.e(TAG, String.format(Locale.getDefault(), "maxRetries: %s, retryCount: %s", + maxRetries, retryCount)); + if (++retryCount < maxRetries) { + return Flowable.timer(retryDelayMillis, + TimeUnit.MILLISECONDS); + } + return Flowable.error(throwable); + } + }); + } +} diff --git a/library/src/main/java/com/chwl/library/rxbus/RxBus.java b/library/src/main/java/com/chwl/library/rxbus/RxBus.java new file mode 100644 index 0000000..f633b2c --- /dev/null +++ b/library/src/main/java/com/chwl/library/rxbus/RxBus.java @@ -0,0 +1,61 @@ +package com.chwl.library.rxbus; + + +import io.reactivex.Flowable; +import io.reactivex.processors.FlowableProcessor; +import io.reactivex.processors.PublishProcessor; +import io.reactivex.subscribers.SerializedSubscriber; + +/** + *

基于RxJava的事件分发封装

+ * + * @author jiahui + * @date 2017/12/12 + */ +public class RxBus { + private final FlowableProcessor mBus; + + private static class Holder { + private static final RxBus BUS = new RxBus(); + } + private RxBus() { + //toSerialized()保证线程安全 + mBus = PublishProcessor.create().toSerialized(); + } + + public static RxBus get() { + return Holder.BUS; + } + + + + /** + * 发送消息 + * + * @param o + */ + public void post(Object o) { + new SerializedSubscriber<>(mBus).onNext(o); + } + + /** + * 确定接收消息类型 + * + * @param tClass 消息类型 + * @param + * @return + */ + public Flowable toFlowable(Class tClass) { + return mBus.ofType(tClass); + } + + /** + * Returns true if the subject has subscribers. + *

The method is thread-safe. + * + * @return true if the subject has subscribers + */ + public boolean hasSubscribers() { + return mBus.hasSubscribers(); + } +} diff --git a/library/src/main/java/com/chwl/library/rxbus/RxBusHelper.java b/library/src/main/java/com/chwl/library/rxbus/RxBusHelper.java new file mode 100644 index 0000000..3fb76bd --- /dev/null +++ b/library/src/main/java/com/chwl/library/rxbus/RxBusHelper.java @@ -0,0 +1,97 @@ +package com.chwl.library.rxbus; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +/** + *

{@link RxBus}简单封装,支持在主线程接收,子线程接收

+ * + * @author jiahui + * @date 2017/12/12 + */ +public class RxBusHelper { + /** + * 发送消息 + * + * @param o + */ + public static void post(Object o) { + RxBus.get().post(o); + } + + private static void doReceiveEvent(Class tClass, CompositeDisposable disposable, + Consumer onNext, Consumer onError, boolean isMainThread) { + if (disposable != null && onError != null) { + disposable.add(RxBus.get().toFlowable(tClass) + .subscribeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .unsubscribeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .observeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .subscribe(onNext, onError) + ); + } else { + if (disposable != null) { + disposable.add(RxBus.get().toFlowable(tClass) +// .compose(RxBus.get().mContext.bindUntilEvent(ActivityEvent.DESTROY)) + .subscribeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .unsubscribeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .observeOn(isMainThread ? AndroidSchedulers.mainThread() : Schedulers.newThread()) + .subscribe(onNext) + ); + } + } + } + + /** + * 接收消息,且在主线程处理 + * + * @param tClass 接收消息类型 + * @param disposable 存放消息 + * @param onNext 处理成功结果回调 + * @param onError 处理错误结果回调 + * @param + */ + public static void doOnMainThread(Class tClass, CompositeDisposable disposable, + Consumer onNext, Consumer onError) { + doReceiveEvent(tClass, disposable, onNext, onError, true); + } + + /** + * 接收消息,且在主线程处理 + * + * @param tClass 接收消息类型 + * @param disposable 存放消息 + * @param onNext 处理成功结果回调 + * @param + */ + public static void doOnMainThread(Class tClass, CompositeDisposable disposable, Consumer onNext) { + doOnMainThread(tClass, disposable, onNext, null); + } + + /** + * 接收消息,且在子线程处理 + * + * @param tClass 接收消息类型 + * @param disposable 存放消息 + * @param onNext 处理成功结果回调 + * @param onError 处理错误结果回调 + * @param + */ + public static void doOnChildThread(Class tClass, CompositeDisposable disposable, + Consumer onNext, Consumer onError) { + doReceiveEvent(tClass, disposable, onNext, onError, false); + } + + /** + * 接收消息,且在子线程处理 + * + * @param tClass 接收消息类型 + * @param disposable 存放消息 + * @param onNext 处理成功结果回调 + * @param + */ + public static void doOnChildThread(Class tClass, CompositeDisposable disposable, Consumer onNext) { + doOnChildThread(tClass, disposable, onNext, null); + } +} diff --git a/library/src/main/java/com/chwl/library/service/ErBanService.java b/library/src/main/java/com/chwl/library/service/ErBanService.java new file mode 100644 index 0000000..7418b59 --- /dev/null +++ b/library/src/main/java/com/chwl/library/service/ErBanService.java @@ -0,0 +1,35 @@ +package com.chwl.library.service; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import com.orhanobut.logger.Logger; +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; + +/** + *

在电量充足,wifi等情况下处理后台耗时任务

+ * + * @author jiahui + * date 2018/3/6 + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class ErBanService extends JobService { + + @Override + public boolean onStartJob(JobParameters params) { + int jobId = params.getJobId(); + Logger.d(ResUtil.getString(R.string.xchat_android_library_service_erbanservice_01), jobId); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + int jobId = params.getJobId(); + Logger.d(ResUtil.getString(R.string.xchat_android_library_service_erbanservice_05), jobId); + return false; + } +} diff --git a/library/src/main/java/com/chwl/library/softinput/SoftHideKeyBoardUtil.java b/library/src/main/java/com/chwl/library/softinput/SoftHideKeyBoardUtil.java new file mode 100644 index 0000000..5a275b4 --- /dev/null +++ b/library/src/main/java/com/chwl/library/softinput/SoftHideKeyBoardUtil.java @@ -0,0 +1,58 @@ +package com.chwl.library.softinput; + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +public class SoftHideKeyBoardUtil { + private View mChildOfContent; + private int usableHeightPrevious; + private FrameLayout.LayoutParams frameLayoutParams; + + private SoftHideKeyBoardUtil(Activity activity) { + //找到DecorView + FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content); + //获取到用户设置的View + mChildOfContent = content.getChildAt(0); + mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + possiblyResizeChildOfContent(); + } + }); + + frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); + } + + private void possiblyResizeChildOfContent() { + int usableHeightNow = computeUsableHeight(); + + if (usableHeightNow != usableHeightPrevious) { + int usableHeightSansKeyBoard = mChildOfContent.getRootView().getHeight(); + //Activity中xml布局的高度 + int heightDifference = usableHeightSansKeyBoard - usableHeightNow; + if (heightDifference > 100 /*(usableHeightSansKeyBoard / 4)*/) { + frameLayoutParams.height = usableHeightSansKeyBoard - heightDifference; + } else { + frameLayoutParams.height = usableHeightSansKeyBoard; + } + + mChildOfContent.requestLayout(); + usableHeightPrevious = usableHeightNow; + } + } + + private int computeUsableHeight() { + Rect rect = new Rect(); + mChildOfContent.getWindowVisibleDisplayFrame(rect); + //全屏时,直接返回rect.bottom,rect.top是状态栏的高度 +// return (rect.bottom - rect.top); + return rect.bottom; + } + + public static void assistActivity(Activity activity) { + new SoftHideKeyBoardUtil(activity); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/swipeactivity/SwipeActivityUtils.java b/library/src/main/java/com/chwl/library/swipeactivity/SwipeActivityUtils.java new file mode 100644 index 0000000..83a31d5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/SwipeActivityUtils.java @@ -0,0 +1,105 @@ + +package com.chwl.library.swipeactivity; + +import android.app.Activity; +import android.app.ActivityOptions; +import android.os.Build; + +import java.lang.reflect.Method; + +/** + * Created by hm + */ +public class SwipeActivityUtils { + private SwipeActivityUtils() { + } + + /** + * Convert a translucent themed Activity + * {@link android.R.attr#windowIsTranslucent} to a fullscreen opaque + * Activity. + *

+ * Call this whenever the background of a translucent Activity has changed + * to become opaque. Doing so will allow the {@link android.view.Surface} of + * the Activity behind to be released. + *

+ * This call has no effect on non-translucent activities or on activities + * with the {@link android.R.attr#windowIsFloating} attribute. + */ + public static void convertActivityFromTranslucent(Activity activity) { + try { + Method method = Activity.class.getDeclaredMethod("convertFromTranslucent"); + method.setAccessible(true); + method.invoke(activity); + } catch (Throwable t) { + } + } + + /** + * Convert a translucent themed Activity + * {@link android.R.attr#windowIsTranslucent} back from opaque to + * translucent following a call to + * {@link #convertActivityFromTranslucent(Activity)} . + *

+ * Calling this allows the Activity behind this one to be seen again. Once + * all such Activities have been redrawn + *

+ * This call has no effect on non-translucent activities or on activities + * with the {@link android.R.attr#windowIsFloating} attribute. + */ + public static void convertActivityToTranslucent(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + convertActivityToTranslucentAfterL(activity); + } else { + convertActivityToTranslucentBeforeL(activity); + } + } + + /** + * Calling the convertToTranslucent method on platforms before Android 5.0 + */ + @SuppressWarnings("rawtypes") + public static void convertActivityToTranslucentBeforeL(Activity activity) { + try { + Class[] classes = Activity.class.getDeclaredClasses(); + Class translucentConversionListenerClazz = null; + for (Class clazz : classes) { + if (clazz.getSimpleName().contains("TranslucentConversionListener")) { + translucentConversionListenerClazz = clazz; + } + } + Method method = Activity.class.getDeclaredMethod("convertToTranslucent", + translucentConversionListenerClazz); + method.setAccessible(true); + method.invoke(activity, new Object[] { + null + }); + } catch (Throwable t) { + } + } + + /** + * Calling the convertToTranslucent method on platforms after Android 5.0 + */ + @SuppressWarnings("rawtypes") + private static void convertActivityToTranslucentAfterL(Activity activity) { + try { + Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions"); + getActivityOptions.setAccessible(true); + Object options = getActivityOptions.invoke(activity); + + Class[] classes = Activity.class.getDeclaredClasses(); + Class translucentConversionListenerClazz = null; + for (Class clazz : classes) { + if (clazz.getSimpleName().contains("TranslucentConversionListener")) { + translucentConversionListenerClazz = clazz; + } + } + Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent", + translucentConversionListenerClazz, ActivityOptions.class); + convertToTranslucent.setAccessible(true); + convertToTranslucent.invoke(activity, null, options); + } catch (Throwable t) { + } + } +} diff --git a/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivity.java b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivity.java new file mode 100644 index 0000000..63661b8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivity.java @@ -0,0 +1,107 @@ + +package com.chwl.library.swipeactivity; + +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; + +import java.lang.reflect.Field; + + +/** + * + *

Title: SwipeBackActivity

+ *

Description: 滑动退出 + * + * @author hm + * @version V1.0

+ * @Date 2017年5月24日 下午5:05:13 + * 修改记录: + * 下面填写修改的内容以及修改的日期 + * + */ +public class SwipeBackActivity extends RxAppCompatActivity implements SwipeBackActivityBase { + private SwipeBackActivityHelper mHelper; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHelper = new SwipeBackActivityHelper(this); + mHelper.onActivityCreate(); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mHelper.onPostCreate(); + } + + @Override + public View findViewById(int id) { + View v = super.findViewById(id); + if (v == null && mHelper != null) + return mHelper.findViewById(id); + return v; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // fixInputMethodManagerLeak(this); + } + + @Override + public SwipeBackLayout getSwipeBackLayout() { + return mHelper.getSwipeBackLayout(); + } + + @Override + public void setSwipeBackEnable(boolean enable) { + getSwipeBackLayout().setEnableGesture(enable); + } + + @Override + public void scrollToFinishActivity() { + SwipeActivityUtils.convertActivityToTranslucent(this); + getSwipeBackLayout().scrollToFinishActivity(); + } + + public static void fixInputMethodManagerLeak(Context destContext) { + if (destContext == null) { + return; + } + + InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + + String [] arr = new String[]{"mCurRootView", "mServedView", "mNextServedView"}; + Field f = null; + Object obj_get = null; + for (int i = 0;i < arr.length;i ++) { + String param = arr[i]; + try{ + f = imm.getClass().getDeclaredField(param); + if (f.isAccessible() == false) { + f.setAccessible(true); + } + obj_get = f.get(imm); + if (obj_get != null && obj_get instanceof View) { + View v_get = (View) obj_get; + if (v_get.getContext() == destContext) { // 被InputMethodManager持有引用的context是想要目标销毁的 + f.set(imm, null); // 置空,破坏掉path to gc节点 + } else { + // 不是想要目标销毁的,即为又进了另一层界面了,不要处理,避免影响原逻辑,也就不用继续for循环了 + break; + } + } + }catch(Throwable t){ + t.printStackTrace(); + } + } + } +} diff --git a/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityBase.java b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityBase.java new file mode 100644 index 0000000..271c2c7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityBase.java @@ -0,0 +1,24 @@ +package com.chwl.library.swipeactivity; + +/** + * @author hm + */ +public interface SwipeBackActivityBase { + /** + * 得到SwipeBackLayout对象 + * @return + */ + public abstract SwipeBackLayout getSwipeBackLayout(); + + /** + * 设置是否可以滑动 + * @param enable + */ + public abstract void setSwipeBackEnable(boolean enable); + + /** + * 自动滑动返回并关闭Activity + */ + public abstract void scrollToFinishActivity(); + +} diff --git a/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityHelper.java b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityHelper.java new file mode 100644 index 0000000..794fdfd --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackActivityHelper.java @@ -0,0 +1,59 @@ +package com.chwl.library.swipeactivity; + +import android.app.Activity; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.LayoutInflater; +import android.view.View; + +import com.chwl.library.R; + +/** + * @author Yrom + */ +public class SwipeBackActivityHelper { + private Activity mActivity; + + private SwipeBackLayout mSwipeBackLayout; + + public SwipeBackActivityHelper(Activity activity) { + mActivity = activity; + } + + @SuppressWarnings("deprecation") + public void onActivityCreate() { + mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + mActivity.getWindow().getDecorView().setBackgroundDrawable(null); + mSwipeBackLayout = (SwipeBackLayout) LayoutInflater.from(mActivity).inflate(R.layout.swipeback_layout, null); + mSwipeBackLayout.addSwipeListener(new SwipeBackLayout.SwipeListener() { + @Override + public void onScrollStateChange(int state, float scrollPercent) { + } + + @Override + public void onEdgeTouch(int edgeFlag) { + SwipeActivityUtils.convertActivityToTranslucent(mActivity); + } + + @Override + public void onScrollOverThreshold() { + + } + }); + } + + public void onPostCreate() { + mSwipeBackLayout.attachToActivity(mActivity); + } + + public View findViewById(int id) { + if (mSwipeBackLayout != null) { + return mSwipeBackLayout.findViewById(id); + } + return null; + } + + public SwipeBackLayout getSwipeBackLayout() { + return mSwipeBackLayout; + } +} diff --git a/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackLayout.java b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackLayout.java new file mode 100644 index 0000000..33ef946 --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/SwipeBackLayout.java @@ -0,0 +1,611 @@ +package com.chwl.library.swipeactivity; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.core.view.ViewCompat; + +import com.chwl.library.R; + +import java.util.ArrayList; +import java.util.List; + +public class SwipeBackLayout extends FrameLayout { + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + private static final int FULL_ALPHA = 255; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = ViewDragHelper.EDGE_LEFT; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = ViewDragHelper.EDGE_RIGHT; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = ViewDragHelper.EDGE_BOTTOM; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * A view is not currently being dragged or animating as a result of a + * fling/snap. + */ + public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; + + /** + * A view is currently being dragged. The position is currently changing as + * a result of user input or simulated user input. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** + * Default threshold of scroll + */ + private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f; + + private static final int OVERSCROLL_DISTANCE = 10; + + private static final int[] EDGE_FLAGS = { + EDGE_LEFT, EDGE_RIGHT, EDGE_BOTTOM, EDGE_ALL + }; + + private int mEdgeFlag; + + /** + * Threshold of scroll, we will close the activity, when scrollPercent over + * this value; + */ + private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD; + + private Activity mActivity; + + private boolean mEnable = true; + + private View mContentView; + + private ViewDragHelper mDragHelper; + + private float mScrollPercent; + + private int mContentLeft; + + private int mContentTop; + + /** + * The set of listeners to be sent events through. + */ + private List mListeners; + + private Drawable mShadowLeft; + + private Drawable mShadowRight; + + private Drawable mShadowBottom; + + private float mScrimOpacity; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + + private boolean mInLayout; + + private Rect mTmpRect = new Rect(); + + /** + * Edge being dragged + */ + private int mTrackingEdge; + + public SwipeBackLayout(Context context) { + this(context, null); + } + + public SwipeBackLayout(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.SwipeBackLayoutStyle); + } + + public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + mDragHelper = ViewDragHelper.create(this, new ViewDragCallback()); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle, + R.style.SwipeBackLayout); + + int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1); + if (edgeSize > 0) + setEdgeSize(edgeSize); + int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)]; + setEdgeTrackingEnabled(mode); + + int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left, + R.drawable.shadow_left); + int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right, + R.drawable.shadow_right); + int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom, + R.drawable.shadow_bottom); + setShadow(shadowLeft, EDGE_LEFT); + setShadow(shadowRight, EDGE_RIGHT); + setShadow(shadowBottom, EDGE_BOTTOM); + a.recycle(); + final float density = getResources().getDisplayMetrics().density; + final float minVel = MIN_FLING_VELOCITY * density; + mDragHelper.setMinVelocity(minVel); + mDragHelper.setMaxVelocity(minVel * 2f); + } + + /** + * Sets the sensitivity of the NavigationLayout. + * + * @param context The application context. + * @param sensitivity value between 0 and 1, the final value for touchSlop = + * ViewConfiguration.getScaledTouchSlop * (1 / s); + */ + public void setSensitivity(Context context, float sensitivity) { + mDragHelper.setSensitivity(context, sensitivity); + } + + /** + * Set up contentView which will be moved by user gesture + * + * @param view + */ + private void setContentView(View view) { + mContentView = view; + } + + public void setEnableGesture(boolean enable) { + mEnable = enable; + } + + /** + * Enable edge tracking for the selected edges of the parent view. The + * callback's + * and + * methods will only be invoked for edges for which edge tracking has been + * enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mEdgeFlag = edgeFlags; + mDragHelper.setEdgeTrackingEnabled(mEdgeFlag); + } + + /** + * Set a color to use for the scrim that obscures primary content while a + * drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set the size of an edge. This is the range in pixels along the edges of + * this view that will actively detect edge touches or drags if edge + * tracking is enabled. + * + * @param size The size of an edge in pixels + */ + public void setEdgeSize(int size) { + mDragHelper.setEdgeSize(size); + } + + /** + * Register a callback to be invoked when a swipe event is sent to this + * view. + * + * @param listener the swipe listener to attach to this view + * @deprecated use {@link #addSwipeListener} instead + */ + @Deprecated + public void setSwipeListener(SwipeListener listener) { + addSwipeListener(listener); + } + + /** + * Add a callback to be invoked when a swipe event is sent to this view. + * + * @param listener the swipe listener to attach to this view + */ + public void addSwipeListener(SwipeListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set of listeners + * + * @param listener + */ + public void removeSwipeListener(SwipeListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + } + + public static interface SwipeListener { + /** + * Invoke when state change + * + * @param state flag to describe scroll state + * @param scrollPercent scroll percent of this view + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onScrollStateChange(int state, float scrollPercent); + + /** + * Invoke when edge touched + * + * @param edgeFlag edge flag describing the edge being touched + * @see #EDGE_LEFT + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouch(int edgeFlag); + + /** + * Invoke when scroll percent over the threshold for the first time + */ + public void onScrollOverThreshold(); + } + + /** + * Set scroll threshold, we will close the activity, when scrollPercent over + * this value + * + * @param threshold + */ + public void setScrollThresHold(float threshold) { + if (threshold >= 1.0f || threshold <= 0) { + throw new IllegalArgumentException("Threshold value should be between 0 and 1.0"); + } + mScrollThreshold = threshold; + } + + /** + * Set a drawable used for edge shadow. + * + * @param shadow Drawable to use + * @see #EDGE_LEFT + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setShadow(Drawable shadow, int edgeFlag) { + if ((edgeFlag & EDGE_LEFT) != 0) { + mShadowLeft = shadow; + } else if ((edgeFlag & EDGE_RIGHT) != 0) { + mShadowRight = shadow; + } else if ((edgeFlag & EDGE_BOTTOM) != 0) { + mShadowBottom = shadow; + } + invalidate(); + } + + /** + * Set a drawable used for edge shadow. + * + * @param resId Resource of drawable to use + * @see #EDGE_LEFT + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setShadow(int resId, int edgeFlag) { + setShadow(getResources().getDrawable(resId), edgeFlag); + } + + /** + * Scroll out contentView and finish the activity + */ + public void scrollToFinishActivity() { + final int childWidth = mContentView.getWidth(); + final int childHeight = mContentView.getHeight(); + + int left = 0, top = 0; + if ((mEdgeFlag & EDGE_LEFT) != 0) { + left = childWidth + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE; + mTrackingEdge = EDGE_LEFT; + } else if ((mEdgeFlag & EDGE_RIGHT) != 0) { + left = -childWidth - mShadowRight.getIntrinsicWidth() - OVERSCROLL_DISTANCE; + mTrackingEdge = EDGE_RIGHT; + } else if ((mEdgeFlag & EDGE_BOTTOM) != 0) { + top = -childHeight - mShadowBottom.getIntrinsicHeight() - OVERSCROLL_DISTANCE; + mTrackingEdge = EDGE_BOTTOM; + } + + mDragHelper.smoothSlideViewTo(mContentView, left, top); + invalidate(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (!mEnable) { + return false; + } + try { + return mDragHelper.shouldInterceptTouchEvent(event); + } catch (ArrayIndexOutOfBoundsException e) { + // FIXME: handle exception + // issues #9 + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mEnable) { + return false; + } + mDragHelper.processTouchEvent(event); + return true; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mInLayout = true; + if (mContentView != null) + try { + mContentView.layout(mContentLeft, mContentTop, + mContentLeft + mContentView.getMeasuredWidth(), + mContentTop + mContentView.getMeasuredHeight()); + }catch (Exception e){ + } + + mInLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final boolean drawContent = child == mContentView; + + boolean ret = super.drawChild(canvas, child, drawingTime); + if (mScrimOpacity > 0 && drawContent + && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { + drawShadow(canvas, child); + drawScrim(canvas, child); + } + return ret; + } + + private void drawScrim(Canvas canvas, View child) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int alpha = (int) (baseAlpha * mScrimOpacity); + final int color = alpha << 24 | (mScrimColor & 0xffffff); + + if ((mTrackingEdge & EDGE_LEFT) != 0) { + canvas.clipRect(0, 0, child.getLeft(), getHeight()); + } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { + canvas.clipRect(child.getRight(), 0, getRight(), getHeight()); + } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { + canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight()); + } + canvas.drawColor(color); + } + + private void drawShadow(Canvas canvas, View child) { + final Rect childRect = mTmpRect; + child.getHitRect(childRect); + + if ((mEdgeFlag & EDGE_LEFT) != 0) { + mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top, + childRect.left, childRect.bottom); + mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); + mShadowLeft.draw(canvas); + } + + if ((mEdgeFlag & EDGE_RIGHT) != 0) { + mShadowRight.setBounds(childRect.right, childRect.top, + childRect.right + mShadowRight.getIntrinsicWidth(), childRect.bottom); + mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); + mShadowRight.draw(canvas); + } + + if ((mEdgeFlag & EDGE_BOTTOM) != 0) { + mShadowBottom.setBounds(childRect.left, childRect.bottom, childRect.right, + childRect.bottom + mShadowBottom.getIntrinsicHeight()); + mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); + mShadowBottom.draw(canvas); + } + } + + public void attachToActivity(Activity activity) { + mActivity = activity; + TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{ + android.R.attr.windowBackground + }); + int background = a.getResourceId(0, 0); + a.recycle(); + + ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); + ViewGroup decorChild = (ViewGroup) decor.getChildAt(0); + decorChild.setBackgroundResource(background); + decor.removeView(decorChild); + addView(decorChild); + setContentView(decorChild); + decor.addView(this); + } + + @Override + public void computeScroll() { + mScrimOpacity = 1 - mScrollPercent; + if (mDragHelper.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private boolean mIsScrollOverValid; + + @Override + public boolean tryCaptureView(View view, int i) { + boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i); + if (ret) { + if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) { + mTrackingEdge = EDGE_LEFT; + } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) { + mTrackingEdge = EDGE_RIGHT; + } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) { + mTrackingEdge = EDGE_BOTTOM; + } + if (mListeners != null && !mListeners.isEmpty()) { + for (SwipeListener listener : mListeners) { + listener.onEdgeTouch(mTrackingEdge); + } + } + mIsScrollOverValid = true; + } + boolean directionCheck = false; + if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) { + directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, i); + } else if (mEdgeFlag == EDGE_BOTTOM) { + directionCheck = !mDragHelper + .checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, i); + } else if (mEdgeFlag == EDGE_ALL) { + directionCheck = true; + } + return ret & directionCheck; + } + + @Override + public int getViewHorizontalDragRange(View child) { + return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT); + } + + @Override + public int getViewVerticalDragRange(View child) { + return mEdgeFlag & EDGE_BOTTOM; + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + super.onViewPositionChanged(changedView, left, top, dx, dy); + if ((mTrackingEdge & EDGE_LEFT) != 0) { + mScrollPercent = Math.abs((float) left + / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth())); + } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { + mScrollPercent = Math.abs((float) left + / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth())); + } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { + mScrollPercent = Math.abs((float) top + / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight())); + } + mContentLeft = left; + mContentTop = top; + invalidate(); + if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) { + mIsScrollOverValid = true; + } + if (mListeners != null && !mListeners.isEmpty() + && mDragHelper.getViewDragState() == STATE_DRAGGING + && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) { + mIsScrollOverValid = false; + for (SwipeListener listener : mListeners) { + listener.onScrollOverThreshold(); + } + } + + if (mScrollPercent >= 1) { + if (!mActivity.isFinishing()) { + mActivity.finish(); + mActivity.overridePendingTransition(0, 0); + } + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + final int childWidth = releasedChild.getWidth(); + final int childHeight = releasedChild.getHeight(); + + int left = 0, top = 0; + if ((mTrackingEdge & EDGE_LEFT) != 0) { + left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth + + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0; + } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { + left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth + + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0; + } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { + top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight + + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0; + } + + mDragHelper.settleCapturedViewAt(left, top); + invalidate(); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + int ret = 0; + if ((mTrackingEdge & EDGE_LEFT) != 0) { + ret = Math.min(child.getWidth(), Math.max(left, 0)); + } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { + ret = Math.min(0, Math.max(left, -child.getWidth())); + } + return ret; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + int ret = 0; + if ((mTrackingEdge & EDGE_BOTTOM) != 0) { + ret = Math.min(0, Math.max(top, -child.getHeight())); + } + return ret; + } + + @Override + public void onViewDragStateChanged(int state) { + super.onViewDragStateChanged(state); + if (mListeners != null && !mListeners.isEmpty()) { + for (SwipeListener listener : mListeners) { + listener.onScrollStateChange(state, mScrollPercent); + } + } + } + } +} diff --git a/library/src/main/java/com/chwl/library/swipeactivity/ViewDragHelper.java b/library/src/main/java/com/chwl/library/swipeactivity/ViewDragHelper.java new file mode 100644 index 0000000..84673ef --- /dev/null +++ b/library/src/main/java/com/chwl/library/swipeactivity/ViewDragHelper.java @@ -0,0 +1,1581 @@ +/* + * Copyright (C) 2013 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.chwl.library.swipeactivity; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import androidx.core.view.MotionEventCompat; +import androidx.core.view.VelocityTrackerCompat; +import androidx.core.view.ViewCompat; +import androidx.core.widget.ScrollerCompat; + +import java.util.Arrays; + +/** + * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a + * number of useful operations and state tracking for allowing a user to drag + * and reposition views within their parent ViewGroup. + */ +public class ViewDragHelper { +// private static final String TAG = "ViewDragHelper"; + + /** + * A null/invalid pointer ID. + */ + public static final int INVALID_POINTER = -1; + + /** + * A view is not currently being dragged or animating as a result of a + * fling/snap. + */ + public static final int STATE_IDLE = 0; + + /** + * A view is currently being dragged. The position is currently changing as + * a result of user input or simulated user input. + */ + public static final int STATE_DRAGGING = 1; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = 2; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = 1 << 0; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = 1 << 1; + + /** + * Edge flag indicating that the top edge should be affected. + */ + public static final int EDGE_TOP = 1 << 2; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = 1 << 3; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * Indicates that a check should occur along the horizontal axis + */ + public static final int DIRECTION_HORIZONTAL = 1 << 0; + + /** + * Indicates that a check should occur along the vertical axis + */ + public static final int DIRECTION_VERTICAL = 1 << 1; + + /** + * Indicates that a check should occur along all axes + */ + public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + public static final int EDGE_SIZE = 20; // dp + + private static final int BASE_SETTLE_DURATION = 256; // ms + + private static final int MAX_SETTLE_DURATION = 600; // ms + + // Current drag state; idle, dragging or settling + private int mDragState; + + // Distance to travel before a drag may begin + private int mTouchSlop; + + // Last known position/pointer tracking + private int mActivePointerId = INVALID_POINTER; + + private float[] mInitialMotionX; + + private float[] mInitialMotionY; + + private float[] mLastMotionX; + + private float[] mLastMotionY; + + private int[] mInitialEdgeTouched; + + private int[] mEdgeDragsInProgress; + + private int[] mEdgeDragsLocked; + + private int mPointersDown; + + private VelocityTracker mVelocityTracker; + + private float mMaxVelocity; + + private float mMinVelocity; + + private int mEdgeSize; + + private int mTrackingEdges; + + private ScrollerCompat mScroller; + + private final Callback mCallback; + + private View mCapturedView; + + private boolean mReleaseInProgress; + + private final ViewGroup mParentView; + + /** + * A Callback is used as a communication channel with the ViewDragHelper + * back to the parent view using it. on*methods are invoked on + * siginficant events and several accessor methods are expected to provide + * the ViewDragHelper with more information about the state of the parent + * view upon request. The callback also makes decisions governing the range + * and draggability of child views. + */ + public static abstract class Callback { + /** + * Called when the drag state changes. See the STATE_* + * constants for more information. + * + * @param state The new drag state + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onViewDragStateChanged(int state) { + } + + /** + * Called when the captured view's position changes as the result of a + * drag or settle. + * + * @param changedView View whose position changed + * @param left New X coordinate of the left edge of the view + * @param top New Y coordinate of the top edge of the view + * @param dx Change in X position from the last call + * @param dy Change in Y position from the last call + */ + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + } + + /** + * Called when a child view is captured for dragging or settling. The ID + * of the pointer currently dragging the captured view is supplied. If + * activePointerId is identified as {@link #INVALID_POINTER} the capture + * is programmatic instead of pointer-initiated. + * + * @param capturedChild Child view that was captured + * @param activePointerId Pointer id tracking the child capture + */ + public void onViewCaptured(View capturedChild, int activePointerId) { + } + + /** + * Called when the child view is no longer being actively dragged. The + * fling velocity is also supplied, if relevant. The velocity values may + * be clamped to system minimums or maximums. + *

+ * Calling code may decide to fling or otherwise release the view to let + * it settle into place. It should do so using + * {@link #settleCapturedViewAt(int, int)} or + * {@link #flingCapturedView(int, int, int, int)}. If the Callback + * invokes one of these methods, the ViewDragHelper will enter + * {@link #STATE_SETTLING} and the view capture will not fully end until + * it comes to a complete stop. If neither of these methods is invoked + * before onViewReleased returns, the view will stop in + * place and the ViewDragHelper will return to {@link #STATE_IDLE}. + *

+ * + * @param releasedChild The captured child view now being released + * @param xvel X velocity of the pointer as it left the screen in pixels + * per second. + * @param yvel Y velocity of the pointer as it left the screen in pixels + * per second. + */ + public void onViewReleased(View releasedChild, float xvel, float yvel) { + } + + /** + * Called when one of the subscribed edges in the parent view has been + * touched by the user while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) + * currently touched + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouched(int edgeFlags, int pointerId) { + } + + /** + * Called when the given edge may become locked. This can happen if an + * edge drag was preliminarily rejected before beginning, but after + * {@link #onEdgeTouched(int, int)} was called. This method should + * return true to lock this edge or false to leave it unlocked. The + * default behavior is to leave edges unlocked. + * + * @param edgeFlags A combination of edge flags describing the edge(s) + * locked + * @return true to lock the edge, false to leave it unlocked + */ + public boolean onEdgeLock(int edgeFlags) { + return false; + } + + /** + * Called when the user has started a deliberate drag away from one of + * the subscribed edges in the parent view while no child view is + * currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) + * dragged + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + } + + /** + * Called to determine the Z-order of child views. + * + * @param index the ordered position to query for + * @return index of the view that should be ordered at position + * index + */ + public int getOrderedChildIndex(int index) { + return index; + } + + /** + * Return the magnitude of a draggable child view's horizontal range of + * motion in pixels. This method should return 0 for views that cannot + * move horizontally. + * + * @param child Child view to check + * @return range of horizontal motion in pixels + */ + public int getViewHorizontalDragRange(View child) { + return 0; + } + + /** + * Return the magnitude of a draggable child view's vertical range of + * motion in pixels. This method should return 0 for views that cannot + * move vertically. + * + * @param child Child view to check + * @return range of vertical motion in pixels + */ + public int getViewVerticalDragRange(View child) { + return 0; + } + + /** + * Called when the user's input indicates that they want to capture the + * given child view with the pointer indicated by pointerId. The + * callback should return true if the user is permitted to drag the + * given view with the indicated pointer. + *

+ * ViewDragHelper may call this method multiple times for the same view + * even if the view is already captured; this indicates that a new + * pointer is trying to take control of the view. + *

+ *

+ * If this method returns true, a call to + * {@link #onViewCaptured(View, int)} will follow if the + * capture is successful. + *

+ * + * @param child Child the user is attempting to capture + * @param pointerId ID of the pointer attempting the capture + * @return true if capture should be allowed, false otherwise + */ + public abstract boolean tryCaptureView(View child, int pointerId); + + /** + * Restrict the motion of the dragged child view along the horizontal + * axis. The default implementation does not allow horizontal motion; + * the extending class must override this method and provide the desired + * clamping. + * + * @param child Child view being dragged + * @param left Attempted motion along the X axis + * @param dx Proposed change in position for left + * @return The new clamped position for left + */ + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + /** + * Restrict the motion of the dragged child view along the vertical + * axis. The default implementation does not allow vertical motion; the + * extending class must override this method and provide the desired + * clamping. + * + * @param child Child view being dragged + * @param top Attempted motion along the Y axis + * @param dy Proposed change in position for top + * @return The new clamped position for top + */ + public int clampViewPositionVertical(View child, int top, int dy) { + return 0; + } + } + + /** + * Interpolator defining the animation curve for mScroller + */ + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final Runnable mSetIdleRunnable = new Runnable() { + public void run() { + setDragState(STATE_IDLE); + } + }; + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + return new ViewDragHelper(forParent.getContext(), forParent, cb); + } + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param sensitivity Multiplier for how sensitive the helper should be + * about detecting the start of a drag. Larger values are more + * sensitive. 1.0f is normal. + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + final ViewDragHelper helper = create(forParent, cb); + helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); + return helper; + } + + /** + * Apps should use ViewDragHelper.create() to get a new instance. This will + * allow VDH to use internal compatibility implementations for different + * platform versions. + * + * @param context Context to initialize config-dependent params from + * @param forParent Parent view to monitor + */ + private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + if (forParent == null) { + throw new IllegalArgumentException("Parent view may not be null"); + } + if (cb == null) { + throw new IllegalArgumentException("Callback may not be null"); + } + + mParentView = forParent; + mCallback = cb; + + final ViewConfiguration vc = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); + + mTouchSlop = vc.getScaledTouchSlop(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = ScrollerCompat.create(context, sInterpolator); + } + + /** + * Sets the sensitivity of the dragger. + * + * @param context The application context. + * @param sensitivity value between 0 and 1, the final value for touchSlop = + * ViewConfiguration.getScaledTouchSlop * (1 / s); + */ + public void setSensitivity(Context context, float sensitivity) { + float s = Math.max(0f, Math.min(1.0f, sensitivity)); + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + mTouchSlop = (int) (viewConfiguration.getScaledTouchSlop() * (1 / s)); + } + + /** + * Set the minimum velocity that will be detected as having a magnitude + * greater than zero in pixels per second. Callback methods accepting a + * velocity will be clamped appropriately. + * + * @param minVel minimum velocity to detect + */ + public void setMinVelocity(float minVel) { + mMinVelocity = minVel; + } + + /** + * Set the max velocity that will be detected as having a magnitude + * greater than zero in pixels per second. Callback methods accepting a + * velocity will be clamped appropriately. + * + * @param maxVel max velocity to detect + */ + public void setMaxVelocity(float maxVel) { + mMaxVelocity = maxVel; + } + + /** + * Return the currently configured minimum velocity. Any flings with a + * magnitude less than this value in pixels per second. Callback methods + * accepting a velocity will receive zero as a velocity value if the real + * detected velocity was below this threshold. + * + * @return the minimum velocity that will be detected + */ + public float getMinVelocity() { + return mMinVelocity; + } + + /** + * Retrieve the current drag state of this helper. This will return one of + * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @return The current drag state + */ + public int getViewDragState() { + return mDragState; + } + + /** + * Enable edge tracking for the selected edges of the parent view. The + * callback's + * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeTouched(int, int)} + * and + * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeDragStarted(int, int)} + * methods will only be invoked for edges for which edge tracking has been + * enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mTrackingEdges = edgeFlags; + } + + /** + * Return the size of an edge. This is the range in pixels along the edges + * of this view that will actively detect edge touches or drags if edge + * tracking is enabled. + * + * @return The size of an edge in pixels + * @see #setEdgeTrackingEnabled(int) + */ + public int getEdgeSize() { + return mEdgeSize; + } + + /** + * Set the size of an edge. This is the range in pixels along the edges of + * this view that will actively detect edge touches or drags if edge + * tracking is enabled. + * + * @param size The size of an edge in pixels + */ + public void setEdgeSize(int size) { + mEdgeSize = size; + } + + /** + * Capture a specific child view for dragging within the parent. The + * callback will be notified but + * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#tryCaptureView(View, int)} + * will not be asked permission to capture this view. + * + * @param childView Child view to capture + * @param activePointerId ID of the pointer that is dragging the captured + * child view + */ + public void captureChildView(View childView, int activePointerId) { + if (childView.getParent() != mParentView) { + throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); + } + + mCapturedView = childView; + mActivePointerId = activePointerId; + mCallback.onViewCaptured(childView, activePointerId); + setDragState(STATE_DRAGGING); + } + + /** + * @return The currently captured view, or null if no view has been + * captured. + */ + public View getCapturedView() { + return mCapturedView; + } + + /** + * @return The ID of the pointer currently dragging the captured view, or + * {@link #INVALID_POINTER}. + */ + public int getActivePointerId() { + return mActivePointerId; + } + + /** + * @return The minimum distance in pixels that the user must travel to + * initiate a drag + */ + public int getTouchSlop() { + return mTouchSlop; + } + + /** + * The result of a call to this method is equivalent to + * {@link #processTouchEvent(MotionEvent)} receiving an + * ACTION_CANCEL event. + */ + public void cancel() { + mActivePointerId = INVALID_POINTER; + clearMotionHistory(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * {@link #cancel()}, but also abort all motion in progress and snap to the + * end of any animation. + */ + public void abort() { + cancel(); + if (mDragState == STATE_SETTLING) { + final int oldX = mScroller.getCurrX(); + final int oldY = mScroller.getCurrY(); + mScroller.abortAnimation(); + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); + } + setDragState(STATE_IDLE); + } + + /** + * Animate the view child to the given (left, top) position. If + * this method returns true, the caller should invoke + * {@link #continueSettling(boolean)} on each subsequent frame to continue + * the motion until it returns false. If this method returns false there is + * no further work to do to complete the movement. + *

+ * This operation does not count as a capture event, though + * {@link #getCapturedView()} will still report the sliding view while the + * slide is in progress. + *

+ * + * @param child Child view to capture and animate + * @param finalLeft Final left position of child + * @param finalTop Final top position of child + * @return true if animation should continue through + * {@link #continueSettling(boolean)} calls + */ + public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + mCapturedView = child; + mActivePointerId = INVALID_POINTER; + + return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); + } + + /** + * Settle the captured view at the given (left, top) position. The + * appropriate velocity from prior motion will be taken into account. If + * this method returns true, the caller should invoke + * {@link #continueSettling(boolean)} on each subsequent frame to continue + * the motion until it returns false. If this method returns false there is + * no further work to do to complete the movement. + * + * @param finalLeft Settled left edge position for the captured view + * @param finalTop Settled top edge position for the captured view + * @return true if animation should continue through + * {@link #continueSettling(boolean)} calls + */ + public boolean settleCapturedViewAt(int finalLeft, int finalTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + + "Callback#onViewReleased"); + } + + return forceSettleCapturedViewAt(finalLeft, finalTop, + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); + } + + /** + * Settle the captured view at the given (left, top) position. + * + * @param finalLeft Target left position for the captured view + * @param finalTop Target top position for the captured view + * @param xvel Horizontal velocity + * @param yvel Vertical velocity + * @return true if animation should continue through + * {@link #continueSettling(boolean)} calls + */ + private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { + final int startLeft = mCapturedView.getLeft(); + final int startTop = mCapturedView.getTop(); + final int dx = finalLeft - startLeft; + final int dy = finalTop - startTop; + + if (dx == 0 && dy == 0) { + // Nothing to do. Send callbacks, be done. + mScroller.abortAnimation(); + setDragState(STATE_IDLE); + return false; + } + + final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); + mScroller.startScroll(startLeft, startTop, dx, dy, duration); + + setDragState(STATE_SETTLING); + return true; + } + + private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { + xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); + yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final int absXVel = Math.abs(xvel); + final int absYVel = Math.abs(yvel); + final int addedVel = absXVel + absYVel; + final int addedDistance = absDx + absDy; + + final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx + / addedDistance; + final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy + / addedDistance; + + int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); + int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); + + return (int) (xduration * xweight + yduration * yweight); + } + + private int computeAxisDuration(int delta, int velocity, int motionRange) { + if (delta == 0) { + return 0; + } + + final int width = mParentView.getWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); + final float distance = halfWidth + halfWidth + * distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float range = (float) Math.abs(delta) / motionRange; + duration = (int) ((range + 1) * BASE_SETTLE_DURATION); + } + return Math.min(duration, MAX_SETTLE_DURATION); + } + + /** + * Clamp the magnitude of value for absMin and absMax. If the value is below + * the minimum, it will be clamped to zero. If the value is above the + * maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private int clampMag(int value, int absMin, int absMax) { + final int absValue = Math.abs(value); + if (absValue < absMin) + return 0; + if (absValue > absMax) + return value > 0 ? absMax : -absMax; + return value; + } + + /** + * Clamp the magnitude of value for absMin and absMax. If the value is below + * the minimum, it will be clamped to zero. If the value is above the + * maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private float clampMag(float value, float absMin, float absMax) { + final float absValue = Math.abs(value); + if (absValue < absMin) + return 0; + if (absValue > absMax) + return value > 0 ? absMax : -absMax; + return value; + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Settle the captured view based on standard free-moving fling behavior. + * The caller should invoke {@link #continueSettling(boolean)} on each + * subsequent frame to continue the motion until it returns false. + * + * @param minLeft Minimum X position for the view's left edge + * @param minTop Minimum Y position for the view's top edge + * @param maxLeft Maximum X position for the view's left edge + * @param maxTop Maximum Y position for the view's top edge + */ + public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + + "Callback#onViewReleased"); + } + + mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + minLeft, maxLeft, minTop, maxTop); + + setDragState(STATE_SETTLING); + } + + /** + * Move the captured settling view by the appropriate amount for the current + * time. If continueSettling returns true, the caller should + * call it again on the next frame to continue. + * + * @param deferCallbacks true if state callbacks should be deferred via + * posted message. Set this to true if you are calling this + * method from {@link View#computeScroll()} or + * similar methods invoked as part of layout or drawing. + * @return true if settle is still in progress + */ + public boolean continueSettling(boolean deferCallbacks) { + if (mDragState == STATE_SETTLING) { + boolean keepGoing = mScroller.computeScrollOffset(); + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + final int dx = x - mCapturedView.getLeft(); + final int dy = y - mCapturedView.getTop(); + + if (dx != 0) { + mCapturedView.offsetLeftAndRight(dx); + } + if (dy != 0) { + mCapturedView.offsetTopAndBottom(dy); + } + + if (dx != 0 || dy != 0) { + mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); + } + + if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { + // Close enough. The interpolator/scroller might think we're + // still moving + // but the user sure doesn't. + mScroller.abortAnimation(); + keepGoing = mScroller.isFinished(); + } + + if (!keepGoing) { + if (deferCallbacks) { + mParentView.post(mSetIdleRunnable); + } else { + setDragState(STATE_IDLE); + } + } + } + + return mDragState == STATE_SETTLING; + } + + /** + * Like all callback events this must happen on the UI thread, but release + * involves some extra semantics. During a release (mReleaseInProgress) is + * the only time it is valid to call {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. + */ + private void dispatchViewReleased(float xvel, float yvel) { + mReleaseInProgress = true; + mCallback.onViewReleased(mCapturedView, xvel, yvel); + mReleaseInProgress = false; + + if (mDragState == STATE_DRAGGING) { + // onViewReleased didn't call a method that would have changed this. + // Go idle. + setDragState(STATE_IDLE); + } + } + + private void clearMotionHistory() { + if (mInitialMotionX == null) { + return; + } + Arrays.fill(mInitialMotionX, 0); + Arrays.fill(mInitialMotionY, 0); + Arrays.fill(mLastMotionX, 0); + Arrays.fill(mLastMotionY, 0); + Arrays.fill(mInitialEdgeTouched, 0); + Arrays.fill(mEdgeDragsInProgress, 0); + Arrays.fill(mEdgeDragsLocked, 0); + mPointersDown = 0; + } + + private void clearMotionHistory(int pointerId) { + if (mInitialMotionX == null) { + return; + } + mInitialMotionX[pointerId] = 0; + mInitialMotionY[pointerId] = 0; + mLastMotionX[pointerId] = 0; + mLastMotionY[pointerId] = 0; + mInitialEdgeTouched[pointerId] = 0; + mEdgeDragsInProgress[pointerId] = 0; + mEdgeDragsLocked[pointerId] = 0; + mPointersDown &= ~(1 << pointerId); + } + + private void ensureMotionHistorySizeForId(int pointerId) { + if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { + float[] imx = new float[pointerId + 1]; + float[] imy = new float[pointerId + 1]; + float[] lmx = new float[pointerId + 1]; + float[] lmy = new float[pointerId + 1]; + int[] iit = new int[pointerId + 1]; + int[] edip = new int[pointerId + 1]; + int[] edl = new int[pointerId + 1]; + + if (mInitialMotionX != null) { + System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); + System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); + System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); + System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); + System.arraycopy(mInitialEdgeTouched, 0, iit, 0, mInitialEdgeTouched.length); + System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); + System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); + } + + mInitialMotionX = imx; + mInitialMotionY = imy; + mLastMotionX = lmx; + mLastMotionY = lmy; + mInitialEdgeTouched = iit; + mEdgeDragsInProgress = edip; + mEdgeDragsLocked = edl; + } + } + + private void saveInitialMotion(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgeTouched[pointerId] = getEdgeTouched((int) x, (int) y); + mPointersDown |= 1 << pointerId; + } + + private void saveLastMotion(MotionEvent ev) { + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + mLastMotionX[pointerId] = x; + mLastMotionY[pointerId] = y; + } + } + + /** + * Check if the given pointer ID represents a pointer that is currently down + * (to the best of the ViewDragHelper's knowledge). + *

+ * The state used to report this information is populated by the methods + * {@link #shouldInterceptTouchEvent(MotionEvent)} or + * {@link #processTouchEvent(MotionEvent)}. If one of these + * methods has not been called for all relevant MotionEvents to track, the + * information reported by this method may be stale or incorrect. + *

+ * + * @param pointerId pointer ID to check; corresponds to IDs provided by + * MotionEvent + * @return true if the pointer with the given ID is still down + */ + public boolean isPointerDown(int pointerId) { + return (mPointersDown & 1 << pointerId) != 0; + } + + void setDragState(int state) { + if (mDragState != state) { + mDragState = state; + mCallback.onViewDragStateChanged(state); + if (state == STATE_IDLE) { + mCapturedView = null; + } + } + } + + /** + * Attempt to capture the view with the given pointer ID. The callback will + * be involved. This will put us into the "dragging" state. If we've already + * captured this view with this pointer this method will immediately return + * true without consulting the callback. + * + * @param toCapture View to capture + * @param pointerId Pointer to capture with + * @return true if capture was successful + */ + boolean tryCaptureViewForDrag(View toCapture, int pointerId) { + if (toCapture == mCapturedView && mActivePointerId == pointerId) { + // Already done! + return true; + } + if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { + mActivePointerId = pointerId; + captureChildView(toCapture, pointerId); + return true; + } + return false; + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for + * scrollability (true), or just its children (false). + * @param dx Delta scrolled in pixels along the X axis + * @param dy Delta scrolled in pixels along the Y axis + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance + // first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() + && x + scrollX < child.getRight() + && y + scrollY >= child.getTop() + && y + scrollY < child.getBottom() + && canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), y + + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV + && (ViewCompat.canScrollHorizontally(v, -dx) || ViewCompat.canScrollVertically(v, + -dy)); + } + + /** + * Check if this event as provided to the parent view's + * onInterceptTouchEvent should cause the parent to intercept the touch + * event stream. + * + * @param ev MotionEvent provided to onInterceptTouchEvent + * @return true if the parent view should return true from + * onInterceptTouchEvent + */ + public boolean shouldInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + saveInitialMotion(x, y, pointerId); + + final View toCapture = findTopChildUnder((int) x, (int) y); + + // Catch a settling view if possible. + if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { + tryCaptureViewForDrag(toCapture, pointerId); + } + + final int edgesTouched = mInitialEdgeTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + final int edgesTouched = mInitialEdgeTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (mDragState == STATE_SETTLING) { + // Catch a settling view if possible. + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture == mCapturedView) { + tryCaptureViewForDrag(toCapture, pointerId); + } + } + break; + } + + case MotionEvent.ACTION_MOVE: { + // First to cross a touch slop over a draggable view wins. Also + // report edge drags. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture != null && checkTouchSlop(toCapture, dx, dy) + && tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + cancel(); + break; + } + } + + return mDragState == STATE_DRAGGING; + } + + /** + * Process a touch event received by the parent view. This method will + * dispatch callback events as needed before returning. The parent view's + * onTouchEvent implementation should call this. + * + * @param ev The touch event received by the parent view + */ + public void processTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + final View toCapture = findTopChildUnder((int) x, (int) y); + + saveInitialMotion(x, y, pointerId); + + // Since the parent is already directly processing this touch + // event, + // there is no reason to delay for a slop before dragging. + // Start immediately if possible. + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgeTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + // If we're idle we can do anything! Treat it like a normal + // down event. + + final View toCapture = findTopChildUnder((int) x, (int) y); + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgeTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (isCapturedViewUnder((int) x, (int) y)) { + // We're still tracking a captured view. If the same view is + // under this + // point, we'll swap to controlling it with this pointer + // instead. + // (This will still work if we're "catching" a settling + // view.) + + tryCaptureViewForDrag(mCapturedView, pointerId); + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mDragState == STATE_DRAGGING) { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, index); + final float y = MotionEventCompat.getY(ev, index); + final int idx = (int) (x - mLastMotionX[mActivePointerId]); + final int idy = (int) (y - mLastMotionY[mActivePointerId]); + + dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); + + saveLastMotion(ev); + } else { + // Check to see if any pointer is now over a draggable view. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag. + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (checkTouchSlop(toCapture, dx, dy) + && tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { + // Try to find another pointer that's still holding on to + // the captured view. + int newActivePointer = INVALID_POINTER; + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int id = MotionEventCompat.getPointerId(ev, i); + if (id == mActivePointerId) { + // This one's going away, skip. + continue; + } + + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + if (findTopChildUnder((int) x, (int) y) == mCapturedView + && tryCaptureViewForDrag(mCapturedView, id)) { + newActivePointer = mActivePointerId; + break; + } + } + + if (newActivePointer == INVALID_POINTER) { + // We didn't find another pointer still touching the + // view, release it. + releaseViewForPointerUp(); + } + } + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: { + if (mDragState == STATE_DRAGGING) { + releaseViewForPointerUp(); + } + cancel(); + break; + } + + case MotionEvent.ACTION_CANCEL: { + if (mDragState == STATE_DRAGGING) { + dispatchViewReleased(0, 0); + } + cancel(); + break; + } + } + } + + private void reportNewEdgeDrags(float dx, float dy, int pointerId) { + int dragsStarted = 0; + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { + dragsStarted |= EDGE_LEFT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { + dragsStarted |= EDGE_TOP; + } + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { + dragsStarted |= EDGE_RIGHT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { + dragsStarted |= EDGE_BOTTOM; + } + + if (dragsStarted != 0) { + mEdgeDragsInProgress[pointerId] |= dragsStarted; + mCallback.onEdgeDragStarted(dragsStarted, pointerId); + } + } + + private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { + final float absDelta = Math.abs(delta); + final float absODelta = Math.abs(odelta); + + if ((mInitialEdgeTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 + || (mEdgeDragsLocked[pointerId] & edge) == edge + || (mEdgeDragsInProgress[pointerId] & edge) == edge + || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { + return false; + } + if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { + mEdgeDragsLocked[pointerId] |= edge; + return false; + } + return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; + } + + /** + * Check if we've crossed a reasonable touch slop for the given child view. + * If the child cannot be dragged along the horizontal or vertical axis, + * motion along that axis will not count toward the slop check. + * + * @param child Child to check + * @param dx Motion since initial position along X axis + * @param dy Motion since initial position along Y axis + * @return true if the touch slop has been crossed + */ + private boolean checkTouchSlop(View child, float dx, float dy) { + if (child == null) { + return false; + } + final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; + final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any pointer tracked in the current gesture has crossed the + * required slop threshold. + *

+ * This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(MotionEvent)} or + * {@link #processTouchEvent(MotionEvent)}. You should only + * rely on the results of this method after all currently available touch + * data has been provided to one of these two methods. + *

+ * + * @param directions Combination of direction flags, see + * {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL}, + * {@link #DIRECTION_ALL} + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions) { + final int count = mInitialMotionX.length; + for (int i = 0; i < count; i++) { + if (checkTouchSlop(directions, i)) { + return true; + } + } + return false; + } + + /** + * Check if the specified pointer tracked in the current gesture has crossed + * the required slop threshold. + *

+ * This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(MotionEvent)} or + * {@link #processTouchEvent(MotionEvent)}. You should only + * rely on the results of this method after all currently available touch + * data has been provided to one of these two methods. + *

+ * + * @param directions Combination of direction flags, see + * {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL}, + * {@link #DIRECTION_ALL} + * @param pointerId ID of the pointer to slop check as specified by + * MotionEvent + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions, int pointerId) { + if (!isPointerDown(pointerId)) { + return false; + } + + final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; + final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; + + final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; + final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any of the edges specified were initially touched in the + * currently active gesture. If there is no currently active gesture this + * method will return false. + * + * @param edges Edges to check for an initial edge touch. See + * {@link #EDGE_LEFT}, {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, + * {@link #EDGE_BOTTOM} and {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the + * current gesture + */ + public boolean isEdgeTouched(int edges) { + final int count = mInitialEdgeTouched.length; + for (int i = 0; i < count; i++) { + if (isEdgeTouched(edges, i)) { + return true; + } + } + return false; + } + + /** + * Check if any of the edges specified were initially touched by the pointer + * with the specified ID. If there is no currently active gesture or if + * there is no pointer with the given ID currently down this method will + * return false. + * + * @param edges Edges to check for an initial edge touch. See + * {@link #EDGE_LEFT}, {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, + * {@link #EDGE_BOTTOM} and {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the + * current gesture + */ + public boolean isEdgeTouched(int edges, int pointerId) { + return isPointerDown(pointerId) && (mInitialEdgeTouched[pointerId] & edges) != 0; + } + + private void releaseViewForPointerUp() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float xvel = clampMag( + VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + final float yvel = clampMag( + VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + dispatchViewReleased(xvel, yvel); + } + + private void dragTo(int left, int top, int dx, int dy) { + int clampedX = left; + int clampedY = top; + final int oldLeft = mCapturedView.getLeft(); + final int oldTop = mCapturedView.getTop(); + if (dx != 0) { + clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); + mCapturedView.offsetLeftAndRight(clampedX - oldLeft); + } + if (dy != 0) { + clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); + mCapturedView.offsetTopAndBottom(clampedY - oldTop); + } + + if (dx != 0 || dy != 0) { + final int clampedDx = clampedX - oldLeft; + final int clampedDy = clampedY - oldTop; + mCallback + .onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); + } + } + + /** + * Determine if the currently captured view is under the given point in the + * parent view's coordinate system. If there is no captured view this method + * will return false. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the captured view is under the given point, false + * otherwise + */ + public boolean isCapturedViewUnder(int x, int y) { + return isViewUnder(mCapturedView, x, y); + } + + /** + * Determine if the supplied view is under the given point in the parent + * view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false + * otherwise + */ + public boolean isViewUnder(View view, int x, int y) { + if (view == null) { + return false; + } + return x >= view.getLeft() && x < view.getRight() && y >= view.getTop() + && y < view.getBottom(); + } + + /** + * Find the topmost child under the given point within the parent view's + * coordinate system. The child order is determined using + * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#getOrderedChildIndex(int)} + * . + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return The topmost child view under (x, y) or null if none found. + */ + public View findTopChildUnder(int x, int y) { + final int childCount = mParentView.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); + if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() + && y < child.getBottom()) { + return child; + } + } + return null; + } + + private int getEdgeTouched(int x, int y) { + int result = 0; + + if (x < mParentView.getLeft() + mEdgeSize) + result = EDGE_LEFT; + if (y < mParentView.getTop() + mEdgeSize) + result = EDGE_TOP; + if (x > mParentView.getRight() - mEdgeSize) + result = EDGE_RIGHT; + if (y > mParentView.getBottom() - mEdgeSize) + result = EDGE_BOTTOM; + + return result; + } +} diff --git a/library/src/main/java/com/chwl/library/threadmgr/SchedulePolicy.java b/library/src/main/java/com/chwl/library/threadmgr/SchedulePolicy.java new file mode 100644 index 0000000..997d143 --- /dev/null +++ b/library/src/main/java/com/chwl/library/threadmgr/SchedulePolicy.java @@ -0,0 +1,21 @@ +package com.chwl.library.threadmgr; + +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * create by lvzebiao @2019/6/27 + */ +public class SchedulePolicy implements RejectedExecutionHandler { + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + Log.e("mouse_debug", ResUtil.getString(R.string.xchat_android_library_threadmgr_schedulepolicy_01)); + } + +} diff --git a/library/src/main/java/com/chwl/library/threadmgr/SpeakPolicy.java b/library/src/main/java/com/chwl/library/threadmgr/SpeakPolicy.java new file mode 100644 index 0000000..9cace11 --- /dev/null +++ b/library/src/main/java/com/chwl/library/threadmgr/SpeakPolicy.java @@ -0,0 +1,21 @@ +package com.chwl.library.threadmgr; + +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * create by lvzebiao @2019/5/27 + */ +public class SpeakPolicy implements RejectedExecutionHandler { + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + Log.e("mouse_debug", ResUtil.getString(R.string.xchat_android_library_threadmgr_speakpolicy_01)); + } + +} diff --git a/library/src/main/java/com/chwl/library/threadmgr/SpeakThreadFactory.java b/library/src/main/java/com/chwl/library/threadmgr/SpeakThreadFactory.java new file mode 100644 index 0000000..5e83de0 --- /dev/null +++ b/library/src/main/java/com/chwl/library/threadmgr/SpeakThreadFactory.java @@ -0,0 +1,17 @@ +package com.chwl.library.threadmgr; + +import androidx.annotation.NonNull; + +import java.util.concurrent.ThreadFactory; + +/** + * create by lvzebiao @2019/5/27 + */ +public class SpeakThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(@NonNull Runnable r) { + return new Thread(r, "speak-thread-pool"); + } + +} diff --git a/library/src/main/java/com/chwl/library/threadmgr/ThreadPoolManager.java b/library/src/main/java/com/chwl/library/threadmgr/ThreadPoolManager.java new file mode 100644 index 0000000..dbe39c1 --- /dev/null +++ b/library/src/main/java/com/chwl/library/threadmgr/ThreadPoolManager.java @@ -0,0 +1,66 @@ +package com.chwl.library.threadmgr; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * create by lvzebiao @2019/5/27 + */ +public class ThreadPoolManager { + + /** + * 光圈的线程池 + */ + private ThreadPoolExecutor speakExecutor; + + /** + * 执行周期任务的线程池 + */ + private ThreadPoolExecutor scheduleExecutor; + + private static final class Helper { + public static final ThreadPoolManager INSTANCE = new ThreadPoolManager(); + } + + private ThreadPoolManager() { + + } + + public void init() { + createSpeakExecutor(); + createSingleExecutor(); + } + + public static ThreadPoolManager instance() { + return Helper.INSTANCE; + } + + private void createSpeakExecutor() { + //不让回收核心线程 + speakExecutor = new ThreadPoolExecutor(10, 20, + 180L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), new SpeakThreadFactory(), new SpeakPolicy()); + } + + private void createSingleExecutor() { + scheduleExecutor = new ThreadPoolExecutor(1, 5, + 180L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), new SchedulePolicy()); + scheduleExecutor.allowCoreThreadTimeOut(true); + } + + public ThreadPoolExecutor getSpeakExecutor() { + if (speakExecutor == null) { + createSpeakExecutor(); + } + return speakExecutor; + } + + public ThreadPoolExecutor getScheduleExecutor() { + if (scheduleExecutor == null) { + createSingleExecutor(); + } + return scheduleExecutor; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/AppMetaDataUtil.java b/library/src/main/java/com/chwl/library/utils/AppMetaDataUtil.java new file mode 100644 index 0000000..6e82f07 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/AppMetaDataUtil.java @@ -0,0 +1,56 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import com.chwl.library.common.Constants; +import com.chwl.library.utils.config.BasicConfig; +import com.chwl.library.utils.log.MLog; + + +/** + * Created with IntelliJ IDEA. + * User: crid + * Date: 9/3/13 + * Time: 9:23 AM + * To change this template use File | Settings | File Templates. + */ +public class AppMetaDataUtil { + + /** + * @param + * @return 渠道名称 + *

+ * IMPORTANT: 需要在AndroidManifest.xml 配置 + */ + public static String getChannelID() { + String channelID = BasicConfig.INSTANCE.getChannel(); + channelID = TextUtils.isEmpty(channelID) ? Constants.GOOGLE : channelID; + return channelID; + } + + public static String getSvnBuildVersion(Context context) { + return getMetaString(context, "SvnBuildVersion"); + } + + public static String getMetaString(Context context, String key) { + String value = ""; + try { + if (context != null) { + String pkgName = context.getPackageName(); + ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkgName, PackageManager.GET_META_DATA); + value = appInfo.metaData.getString(key); + } + } catch (Exception e) { + MLog.error("AppMetaDataUtil getSvnBuildVersion", e); + } + + return value; + } + + public static String getUpdateId(Context context) { + return getMetaString(context, "UpdateId"); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ArrayUtils.java b/library/src/main/java/com/chwl/library/utils/ArrayUtils.java new file mode 100644 index 0000000..445dbfb --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ArrayUtils.java @@ -0,0 +1,5513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; + +/** + *

Operations on arrays, primitive arrays (like {@code int[]}) and + * primitive wrapper arrays (like {@code Integer[]}).

+ *

+ *

This class tries to handle {@code null} input gracefully. + * An exception will not be thrown for a {@code null} + * array input. However, an Object array that contains a {@code null} + * element may throw an exception. Each method documents its behaviour.

+ *

+ *

#ThreadSafe#

+ * + * @version $Id: ArrayUtils.java 1578214 2014-03-17 02:44:58Z ggregory $ + * @since 2.0 + */ +public class ArrayUtils { + + /** + * An empty immutable {@code Object} array. + */ + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + /** + * An empty immutable {@code Class} array. + */ + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + /** + * An empty immutable {@code String} array. + */ + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + /** + * An empty immutable {@code long} array. + */ + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + /** + * An empty immutable {@code Long} array. + */ + public static final Long[] EMPTY_LONG_OBJECT_ARRAY = new Long[0]; + /** + * An empty immutable {@code int} array. + */ + public static final int[] EMPTY_INT_ARRAY = new int[0]; + /** + * An empty immutable {@code Integer} array. + */ + public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0]; + /** + * An empty immutable {@code short} array. + */ + public static final short[] EMPTY_SHORT_ARRAY = new short[0]; + /** + * An empty immutable {@code Short} array. + */ + public static final Short[] EMPTY_SHORT_OBJECT_ARRAY = new Short[0]; + /** + * An empty immutable {@code byte} array. + */ + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + /** + * An empty immutable {@code Byte} array. + */ + public static final Byte[] EMPTY_BYTE_OBJECT_ARRAY = new Byte[0]; + /** + * An empty immutable {@code double} array. + */ + public static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; + /** + * An empty immutable {@code Double} array. + */ + public static final Double[] EMPTY_DOUBLE_OBJECT_ARRAY = new Double[0]; + /** + * An empty immutable {@code float} array. + */ + public static final float[] EMPTY_FLOAT_ARRAY = new float[0]; + /** + * An empty immutable {@code Float} array. + */ + public static final Float[] EMPTY_FLOAT_OBJECT_ARRAY = new Float[0]; + /** + * An empty immutable {@code boolean} array. + */ + public static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; + /** + * An empty immutable {@code Boolean} array. + */ + public static final Boolean[] EMPTY_BOOLEAN_OBJECT_ARRAY = new Boolean[0]; + /** + * An empty immutable {@code char} array. + */ + public static final char[] EMPTY_CHAR_ARRAY = new char[0]; + /** + * An empty immutable {@code Character} array. + */ + public static final Character[] EMPTY_CHARACTER_OBJECT_ARRAY = new Character[0]; + + /** + * The index value when an element is not found in a list or array: {@code -1}. + * This value is returned by methods in this class and can also be used in comparisons with values returned by + * various method from {@link java.util.List}. + */ + public static final int INDEX_NOT_FOUND = -1; + + /** + *

ArrayUtils instances should NOT be constructed in standard programming. + * Instead, the class should be used as ArrayUtils.clone(new int[] {2}).

+ *

+ *

This constructor is public to permit tools that require a JavaBean instance + * to operate.

+ */ + public ArrayUtils() { + super(); + } + + + // NOTE: Cannot use {@code} to enclose text which includes {}, but is OK + + // To map + //----------------------------------------------------------------------- + + /** + *

Converts the given array into a {@link Map}. Each element of the array + * must be either a {@link Map.Entry} or an Array, containing at least two + * elements, where the first element is used as key and the second as + * value.

+ *

+ *

This method can be used to initialize:

+ *
+     * // Create a Map mapping colors.
+     * Map colorMap = MapUtils.toMap(new String[][] {{
+     *     {"RED", "#FF0000"},
+     *     {"GREEN", "#00FF00"},
+     *     {"BLUE", "#0000FF"}});
+     * 
+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array an array whose elements are either a {@link Map.Entry} or + * an Array containing at least two elements, may be {@code null} + * @return a {@code Map} that was created from the array + * @throws IllegalArgumentException if one element of this Array is + * itself an Array containing less then two elements + * @throws IllegalArgumentException if the array contains elements other + * than {@link Map.Entry} and an Array + */ + public static Map toMap(final Object[] array) { + if (array == null) { + return null; + } + final Map map = new HashMap((int) (array.length * 1.5)); + for (int i = 0; i < array.length; i++) { + final Object object = array[i]; + if (object instanceof Map.Entry) { + final Map.Entry entry = (Map.Entry) object; + map.put(entry.getKey(), entry.getValue()); + } else if (object instanceof Object[]) { + final Object[] entry = (Object[]) object; + if (entry.length < 2) { + throw new IllegalArgumentException("Array element " + i + ", '" + + object + + "', has a length less than 2"); + } + map.put(entry[0], entry[1]); + } else { + throw new IllegalArgumentException("Array element " + i + ", '" + + object + + "', is neither of type Map.Entry nor an Array"); + } + } + return map; + } + + // Generic array + //----------------------------------------------------------------------- + + /** + *

Create a type-safe generic array.

+ *

+ *

The Java language does not allow an array to be created from a generic type:

+ *

+ *

+     * public static <T> T[] createAnArray(int size) {
+     * return new T[size]; // compiler error here
+     * }
+     * public static <T> T[] createAnArray(int size) {
+     * return (T[])new Object[size]; // ClassCastException at runtime
+     * }
+     * 
+ *

+ *

Therefore new arrays of generic types can be created with this method. + * For example, an array of Strings can be created:

+ *

+ *

+     * String[] array = ArrayUtils.toArray("1", "2");
+     * String[] emptyArray = ArrayUtils.<String>toArray();
+     * 
+ *

+ *

The method is typically used in scenarios, where the caller itself uses generic types + * that have to be combined into an array.

+ *

+ *

Note, this method makes only sense to provide arguments of the same type so that the + * compiler can deduce the type of the array itself. While it is possible to select the + * type explicitly like in + * Number[] array = ArrayUtils.<Number>toArray(Integer.valueOf(42), Double.valueOf(Math.PI)), + * there is no real advantage when compared to + * new Number[] {Integer.valueOf(42), Double.valueOf(Math.PI)}.

+ * + * @param the array's element type + * @param items the varargs array items, null allowed + * @return the array, not null unless a null array is passed in + * @since 3.0 + */ + public static T[] toArray(final T... items) { + return items; + } + + // Clone + //----------------------------------------------------------------------- + + /** + *

Shallow clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

The objects in the array are not cloned, thus there is no special + * handling for multi-dimensional arrays.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param the component type of the array + * @param array the array to shallow clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static T[] clone(final T[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static long[] clone(final long[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static int[] clone(final int[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static short[] clone(final short[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static char[] clone(final char[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static byte[] clone(final byte[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static double[] clone(final double[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static float[] clone(final float[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static boolean[] clone(final boolean[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + // nullToEmpty + //----------------------------------------------------------------------- + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Object[] nullToEmpty(final Object[] array) { + if (array == null || array.length == 0) { + return EMPTY_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 3.2 + */ + public static Class[] nullToEmpty(final Class[] array) { + if (array == null || array.length == 0) { + return EMPTY_CLASS_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static String[] nullToEmpty(final String[] array) { + if (array == null || array.length == 0) { + return EMPTY_STRING_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static long[] nullToEmpty(final long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static int[] nullToEmpty(final int[] array) { + if (array == null || array.length == 0) { + return EMPTY_INT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static short[] nullToEmpty(final short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static char[] nullToEmpty(final char[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static byte[] nullToEmpty(final byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static double[] nullToEmpty(final double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static float[] nullToEmpty(final float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static boolean[] nullToEmpty(final boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Long[] nullToEmpty(final Long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Integer[] nullToEmpty(final Integer[] array) { + if (array == null || array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Short[] nullToEmpty(final Short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Character[] nullToEmpty(final Character[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Byte[] nullToEmpty(final Byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Double[] nullToEmpty(final Double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Float[] nullToEmpty(final Float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ *

+ *

This method returns an empty array for a {@code null} input array.

+ *

+ *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Boolean[] nullToEmpty(final Boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + return array; + } + + // Subarrays + //----------------------------------------------------------------------- + + /** + *

Produces a new array containing the elements between + * the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ *

+ *

The component type of the subarray is always the same as + * that of the input array. Thus, if the input is an array of type + * {@code Date}, the following usage is envisaged:

+ *

+ *

+     * Date[] someDates = (Date[])ArrayUtils.subarray(allDates, 2, 5);
+     * 
+ * + * @param the component type of the array + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(Object[], int, int) + * @since 2.1 + */ + public static T[] subarray(final T[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + final Class type = array.getClass().getComponentType(); + if (newSize <= 0) { + @SuppressWarnings("unchecked") // OK, because array is of type T + final T[] emptyArray = (T[]) Array.newInstance(type, 0); + return emptyArray; + } + @SuppressWarnings("unchecked") // OK, because array is of type T + final + T[] subarray = (T[]) Array.newInstance(type, newSize); + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code long} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(long[], int, int) + * @since 2.1 + */ + public static long[] subarray(final long[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_LONG_ARRAY; + } + + final long[] subarray = new long[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code int} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(int[], int, int) + * @since 2.1 + */ + public static int[] subarray(final int[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_INT_ARRAY; + } + + final int[] subarray = new int[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code short} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(short[], int, int) + * @since 2.1 + */ + public static short[] subarray(final short[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_SHORT_ARRAY; + } + + final short[] subarray = new short[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code char} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(char[], int, int) + * @since 2.1 + */ + public static char[] subarray(final char[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_CHAR_ARRAY; + } + + final char[] subarray = new char[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code byte} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(byte[], int, int) + * @since 2.1 + */ + public static byte[] subarray(final byte[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_BYTE_ARRAY; + } + + final byte[] subarray = new byte[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code double} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(double[], int, int) + * @since 2.1 + */ + public static double[] subarray(final double[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_DOUBLE_ARRAY; + } + + final double[] subarray = new double[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code float} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(float[], int, int) + * @since 2.1 + */ + public static float[] subarray(final float[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_FLOAT_ARRAY; + } + + final float[] subarray = new float[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code boolean} array containing the elements + * between the start and end indices.

+ *

+ *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @see Arrays#copyOfRange(boolean[], int, int) + * @since 2.1 + */ + public static boolean[] subarray(final boolean[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + final int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_BOOLEAN_ARRAY; + } + + final boolean[] subarray = new boolean[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + // Is same length + //----------------------------------------------------------------------- + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}. + *

+ *

Any multi-dimensional aspects of the arrays are ignored.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final Object[] array1, final Object[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final long[] array1, final long[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final int[] array1, final int[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final short[] array1, final short[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final char[] array1, final char[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final byte[] array1, final byte[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final double[] array1, final double[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final float[] array1, final float[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(final boolean[] array1, final boolean[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + //----------------------------------------------------------------------- + + /** + *

Returns the length of the specified array. + * This method can deal with {@code Object} arrays and with primitive arrays.

+ *

+ *

If the input array is {@code null}, {@code 0} is returned.

+ *

+ *

+     * ArrayUtils.getLength(null)            = 0
+     * ArrayUtils.getLength([])              = 0
+     * ArrayUtils.getLength([null])          = 1
+     * ArrayUtils.getLength([true, false])   = 2
+     * ArrayUtils.getLength([1, 2, 3])       = 3
+     * ArrayUtils.getLength(["a", "b", "c"]) = 3
+     * 
+ * + * @param array the array to retrieve the length from, may be null + * @return The length of the array, or {@code 0} if the array is {@code null} + * @throws IllegalArgumentException if the object argument is not an array. + * @since 2.1 + */ + public static int getLength(final Object array) { + if (array == null) { + return 0; + } + return Array.getLength(array); + } + + /** + *

Checks whether two arrays are the same type taking into account + * multi-dimensional arrays.

+ * + * @param array1 the first array, must not be {@code null} + * @param array2 the second array, must not be {@code null} + * @return {@code true} if type of arrays matches + * @throws IllegalArgumentException if either array is {@code null} + */ + public static boolean isSameType(final Object array1, final Object array2) { + if (array1 == null || array2 == null) { + throw new IllegalArgumentException("The Array must not be null"); + } + return array1.getClass().getName().equals(array2.getClass().getName()); + } + + // Reverse + //----------------------------------------------------------------------- + + /** + *

Reverses the order of the given array.

+ *

+ *

There is no special handling for multi-dimensional arrays.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final Object[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final long[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final int[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final short[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final char[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final byte[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final double[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final float[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

Reverses the order of the given array.

+ *

+ *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(final boolean[] array) { + if (array == null) { + return; + } + reverse(array, 0, array.length); + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final boolean[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + boolean tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final byte[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final char[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + char tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final double[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + double tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final float[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + float tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final int[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + int tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final long[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + long tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final Object[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + Object tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

+ * Reverses the order of the given array in the given range. + *

+ *

+ *

+ * This method does nothing for a {@code null} input array. + *

+ * + * @param array the array to reverse, may be {@code null} + * @param startIndexInclusive the starting index. Undervalue (<0) is promoted to 0, overvalue (>array.length) results in no + * change. + * @param endIndexExclusive elements up to endIndex-1 are reversed in the array. Undervalue (< start index) results in no + * change. Overvalue (>array.length) is demoted to array length. + * @since 3.2 + */ + public static void reverse(final short[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + short tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + // IndexOf search + // ---------------------------------------------------------------------- + + // Object IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given object in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @return the index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final Object[] array, final Object objectToFind) { + return indexOf(array, objectToFind, 0); + } + + /** + *

Finds the index of the given object in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @param startIndex the index to start searching at + * @return the index of the object within the array starting at the index, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final Object[] array, final Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + if (objectToFind == null) { + for (int i = startIndex; i < array.length; i++) { + if (array[i] == null) { + return i; + } + } + } else if (array.getClass().getComponentType().isInstance(objectToFind)) { + for (int i = startIndex; i < array.length; i++) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given object within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @return the last index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final Object[] array, final Object objectToFind) { + return lastIndexOf(array, objectToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given object in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than + * the array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @param startIndex the start index to travers backwards from + * @return the last index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final Object[] array, final Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + if (objectToFind == null) { + for (int i = startIndex; i >= 0; i--) { + if (array[i] == null) { + return i; + } + } + } else if (array.getClass().getComponentType().isInstance(objectToFind)) { + for (int i = startIndex; i >= 0; i--) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the object is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param objectToFind the object to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final Object[] array, final Object objectToFind) { + return indexOf(array, objectToFind) != INDEX_NOT_FOUND; + } + + // long IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final long[] array, final long valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final long[] array, final long valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final long[] array, final long valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final long[] array, final long valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final long[] array, final long valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // int IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final int[] array, final int valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final int[] array, final int valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final int[] array, final int valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final int[] array, final int valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final int[] array, final int valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // short IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final short[] array, final short valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final short[] array, final short valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final short[] array, final short valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final short[] array, final short valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final short[] array, final short valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // char IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int indexOf(final char[] array, final char valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int indexOf(final char[] array, final char valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int lastIndexOf(final char[] array, final char valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int lastIndexOf(final char[] array, final char valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + * @since 2.1 + */ + public static boolean contains(final char[] array, final char valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // byte IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final byte[] array, final byte valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final byte[] array, final byte valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final byte[] array, final byte valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final byte[] array, final byte valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final byte[] array, final byte valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // double IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final double[] array, final double valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value within a given tolerance in the array. + * This method will return the index of the first value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final double[] array, final double valueToFind, final double tolerance) { + return indexOf(array, valueToFind, 0, tolerance); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final double[] array, final double valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the index of the given value in the array starting at the given index. + * This method will return the index of the first value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final double[] array, final double valueToFind, int startIndex, final double tolerance) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + final double min = valueToFind - tolerance; + final double max = valueToFind + tolerance; + for (int i = startIndex; i < array.length; i++) { + if (array[i] >= min && array[i] <= max) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final double[] array, final double valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value within a given tolerance in the array. + * This method will return the index of the last value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final double[] array, final double valueToFind, final double tolerance) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE, tolerance); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final double[] array, final double valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value in the array starting at the given index. + * This method will return the index of the last value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @param tolerance search for value within plus/minus this amount + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final double[] array, final double valueToFind, int startIndex, final double tolerance) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + final double min = valueToFind - tolerance; + final double max = valueToFind + tolerance; + for (int i = startIndex; i >= 0; i--) { + if (array[i] >= min && array[i] <= max) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final double[] array, final double valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + /** + *

Checks if a value falling within the given tolerance is in the + * given array. If the array contains a value within the inclusive range + * defined by (value - tolerance) to (value + tolerance).

+ *

+ *

The method returns {@code false} if a {@code null} array + * is passed in.

+ * + * @param array the array to search + * @param valueToFind the value to find + * @param tolerance the array contains the tolerance of the search + * @return true if value falling within tolerance is in array + */ + public static boolean contains(final double[] array, final double valueToFind, final double tolerance) { + return indexOf(array, valueToFind, 0, tolerance) != INDEX_NOT_FOUND; + } + + // float IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final float[] array, final float valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final float[] array, final float valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final float[] array, final float valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final float[] array, final float valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final float[] array, final float valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // boolean IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the index of the given value in the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(final boolean[] array, final boolean valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} + * array input + */ + public static int indexOf(final boolean[] array, final boolean valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) if + * {@code null} array input.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final boolean[] array, final boolean valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ *

+ *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ *

+ *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than + * the array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(final boolean[] array, final boolean valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ *

+ *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(final boolean[] array, final boolean valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // Primitive/Object array converters + // ---------------------------------------------------------------------- + + // Character array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Characters to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Character} array, may be {@code null} + * @return a {@code char} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static char[] toPrimitive(final Character[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].charValue(); + } + return result; + } + + /** + *

Converts an array of object Character to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Character} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code char} array, {@code null} if null array input + */ + public static char[] toPrimitive(final Character[] array, final char valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + final Character b = array[i]; + result[i] = (b == null ? valueForNull : b.charValue()); + } + return result; + } + + /** + *

Converts an array of primitive chars to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code char} array + * @return a {@code Character} array, {@code null} if null array input + */ + public static Character[] toObject(final char[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + final Character[] result = new Character[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Character.valueOf(array[i]); + } + return result; + } + + // Long array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Longs to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Long} array, may be {@code null} + * @return a {@code long} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static long[] toPrimitive(final Long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].longValue(); + } + return result; + } + + /** + *

Converts an array of object Long to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Long} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code long} array, {@code null} if null array input + */ + public static long[] toPrimitive(final Long[] array, final long valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + final Long b = array[i]; + result[i] = (b == null ? valueForNull : b.longValue()); + } + return result; + } + + /** + *

Converts an array of primitive longs to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code long} array + * @return a {@code Long} array, {@code null} if null array input + */ + public static Long[] toObject(final long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + final Long[] result = new Long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Long.valueOf(array[i]); + } + return result; + } + + // Int array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Integers to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Integer} array, may be {@code null} + * @return an {@code int} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static int[] toPrimitive(final Integer[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].intValue(); + } + return result; + } + + /** + *

Converts an array of object Integer to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Integer} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return an {@code int} array, {@code null} if null array input + */ + public static int[] toPrimitive(final Integer[] array, final int valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + final Integer b = array[i]; + result[i] = (b == null ? valueForNull : b.intValue()); + } + return result; + } + + /** + *

Converts an array of primitive ints to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array an {@code int} array + * @return an {@code Integer} array, {@code null} if null array input + */ + public static Integer[] toObject(final int[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + final Integer[] result = new Integer[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Integer.valueOf(array[i]); + } + return result; + } + + // Short array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Shorts to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Short} array, may be {@code null} + * @return a {@code byte} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static short[] toPrimitive(final Short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].shortValue(); + } + return result; + } + + /** + *

Converts an array of object Short to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Short} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code byte} array, {@code null} if null array input + */ + public static short[] toPrimitive(final Short[] array, final short valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + final Short b = array[i]; + result[i] = (b == null ? valueForNull : b.shortValue()); + } + return result; + } + + /** + *

Converts an array of primitive shorts to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code short} array + * @return a {@code Short} array, {@code null} if null array input + */ + public static Short[] toObject(final short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + final Short[] result = new Short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Short.valueOf(array[i]); + } + return result; + } + + // Byte array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Bytes to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Byte} array, may be {@code null} + * @return a {@code byte} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static byte[] toPrimitive(final Byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].byteValue(); + } + return result; + } + + /** + *

Converts an array of object Bytes to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Byte} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code byte} array, {@code null} if null array input + */ + public static byte[] toPrimitive(final Byte[] array, final byte valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + final Byte b = array[i]; + result[i] = (b == null ? valueForNull : b.byteValue()); + } + return result; + } + + /** + *

Converts an array of primitive bytes to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code byte} array + * @return a {@code Byte} array, {@code null} if null array input + */ + public static Byte[] toObject(final byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + final Byte[] result = new Byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Byte.valueOf(array[i]); + } + return result; + } + + // Double array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Doubles to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Double} array, may be {@code null} + * @return a {@code double} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static double[] toPrimitive(final Double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].doubleValue(); + } + return result; + } + + /** + *

Converts an array of object Doubles to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Double} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code double} array, {@code null} if null array input + */ + public static double[] toPrimitive(final Double[] array, final double valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + final Double b = array[i]; + result[i] = (b == null ? valueForNull : b.doubleValue()); + } + return result; + } + + /** + *

Converts an array of primitive doubles to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code double} array + * @return a {@code Double} array, {@code null} if null array input + */ + public static Double[] toObject(final double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + final Double[] result = new Double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Double.valueOf(array[i]); + } + return result; + } + + // Float array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Floats to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Float} array, may be {@code null} + * @return a {@code float} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static float[] toPrimitive(final Float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].floatValue(); + } + return result; + } + + /** + *

Converts an array of object Floats to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Float} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code float} array, {@code null} if null array input + */ + public static float[] toPrimitive(final Float[] array, final float valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + final Float b = array[i]; + result[i] = (b == null ? valueForNull : b.floatValue()); + } + return result; + } + + /** + *

Converts an array of primitive floats to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code float} array + * @return a {@code Float} array, {@code null} if null array input + */ + public static Float[] toObject(final float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + final Float[] result = new Float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Float.valueOf(array[i]); + } + return result; + } + + // Boolean array converters + // ---------------------------------------------------------------------- + + /** + *

Converts an array of object Booleans to primitives.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Boolean} array, may be {@code null} + * @return a {@code boolean} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static boolean[] toPrimitive(final Boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].booleanValue(); + } + return result; + } + + /** + *

Converts an array of object Booleans to primitives handling {@code null}.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Boolean} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code boolean} array, {@code null} if null array input + */ + public static boolean[] toPrimitive(final Boolean[] array, final boolean valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + final Boolean b = array[i]; + result[i] = (b == null ? valueForNull : b.booleanValue()); + } + return result; + } + + /** + *

Converts an array of primitive booleans to objects.

+ *

+ *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code boolean} array + * @return a {@code Boolean} array, {@code null} if null array input + */ + public static Boolean[] toObject(final boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + final Boolean[] result = new Boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = (array[i] ? Boolean.TRUE : Boolean.FALSE); + } + return result; + } + + // ---------------------------------------------------------------------- + + /** + *

Checks if an array of Objects is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final Object[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive longs is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final long[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive ints is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final int[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive shorts is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final short[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive chars is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final char[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive bytes is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final byte[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive doubles is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final double[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive floats is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final float[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive booleans is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(final boolean[] array) { + return array == null || array.length == 0; + } + + // ---------------------------------------------------------------------- + + /** + *

Checks if an array of Objects is not empty or not {@code null}.

+ * + * @param the component type of the array + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final T[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive longs is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final long[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive ints is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final int[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive shorts is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final short[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive chars is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final char[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive bytes is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final byte[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive doubles is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final double[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive floats is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final float[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive booleans is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(final boolean[] array) { + return (array != null && array.length != 0); + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(null, null)     = null
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * ArrayUtils.addAll([null], [null]) = [null, null]
+     * ArrayUtils.addAll(["a", "b", "c"], ["1", "2", "3"]) = ["a", "b", "c", "1", "2", "3"]
+     * 
+ * + * @param the component type of the array + * @param array1 the first array whose elements are added to the new array, may be {@code null} + * @param array2 the second array whose elements are added to the new array, may be {@code null} + * @return The new array, {@code null} if both arrays are {@code null}. + * The type of the new array is the type of the first array, + * unless the first array is null, in which case the type is the same as the second array. + * @throws IllegalArgumentException if the array types are incompatible + * @since 2.1 + */ + public static T[] addAll(final T[] array1, final T... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final Class type1 = array1.getClass().getComponentType(); + @SuppressWarnings("unchecked") // OK, because array is of type T + final + T[] joinedArray = (T[]) Array.newInstance(type1, array1.length + array2.length); + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + try { + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + } catch (final ArrayStoreException ase) { + // Check if problem was due to incompatible types + /* + * We do this here, rather than before the copy because: + * - it would be a wasted check most of the time + * - safer, in case check turns out to be too strict + */ + final Class type2 = array2.getClass().getComponentType(); + if (!type1.isAssignableFrom(type2)) { + throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of " + + type1.getName(), ase); + } + throw ase; // No, so rethrow original + } + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new boolean[] array. + * @since 2.1 + */ + public static boolean[] addAll(final boolean[] array1, final boolean... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final boolean[] joinedArray = new boolean[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new char[] array. + * @since 2.1 + */ + public static char[] addAll(final char[] array1, final char... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final char[] joinedArray = new char[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new byte[] array. + * @since 2.1 + */ + public static byte[] addAll(final byte[] array1, final byte... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final byte[] joinedArray = new byte[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new short[] array. + * @since 2.1 + */ + public static short[] addAll(final short[] array1, final short... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final short[] joinedArray = new short[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new int[] array. + * @since 2.1 + */ + public static int[] addAll(final int[] array1, final int... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final int[] joinedArray = new int[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new long[] array. + * @since 2.1 + */ + public static long[] addAll(final long[] array1, final long... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final long[] joinedArray = new long[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new float[] array. + * @since 2.1 + */ + public static float[] addAll(final float[] array1, final float... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final float[] joinedArray = new float[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ *

+ *

+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new double[] array. + * @since 2.1 + */ + public static double[] addAll(final double[] array1, final double... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final double[] joinedArray = new double[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element, unless the element itself is null, + * in which case the return type is Object[]

+ *

+ *

+     * ArrayUtils.add(null, null)      = [null]
+     * ArrayUtils.add(null, "a")       = ["a"]
+     * ArrayUtils.add(["a"], null)     = ["a", null]
+     * ArrayUtils.add(["a"], "b")      = ["a", "b"]
+     * ArrayUtils.add(["a", "b"], "c") = ["a", "b", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to "add" the element to, may be {@code null} + * @param element the object to add, may be {@code null} + * @return A new array containing the existing elements plus the new element + * The returned array type will be that of the input array (unless null), + * in which case it will have the same type as the element. + * If both are null, an IllegalArgumentException is thrown + * @throws IllegalArgumentException if both arguments are null + * @since 2.1 + */ + public static T[] add(final T[] array, final T element) { + Class type; + if (array != null) { + type = array.getClass(); + } else if (element != null) { + type = element.getClass(); + } else { + throw new IllegalArgumentException("Arguments cannot both be null"); + } + @SuppressWarnings("unchecked") // type must be T + final + T[] newArray = (T[]) copyArrayGrow1(array, type); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, true)          = [true]
+     * ArrayUtils.add([true], false)       = [true, false]
+     * ArrayUtils.add([true, false], true) = [true, false, true]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static boolean[] add(final boolean[] array, final boolean element) { + final boolean[] newArray = (boolean[]) copyArrayGrow1(array, Boolean.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static byte[] add(final byte[] array, final byte element) { + final byte[] newArray = (byte[]) copyArrayGrow1(array, Byte.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, '0')       = ['0']
+     * ArrayUtils.add(['1'], '0')      = ['1', '0']
+     * ArrayUtils.add(['1', '0'], '1') = ['1', '0', '1']
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static char[] add(final char[] array, final char element) { + final char[] newArray = (char[]) copyArrayGrow1(array, Character.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static double[] add(final double[] array, final double element) { + final double[] newArray = (double[]) copyArrayGrow1(array, Double.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static float[] add(final float[] array, final float element) { + final float[] newArray = (float[]) copyArrayGrow1(array, Float.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static int[] add(final int[] array, final int element) { + final int[] newArray = (int[]) copyArrayGrow1(array, Integer.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static long[] add(final long[] array, final long element) { + final long[] newArray = (long[]) copyArrayGrow1(array, Long.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ *

+ *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static short[] add(final short[] array, final short element) { + final short[] newArray = (short[]) copyArrayGrow1(array, Short.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + * Returns a copy of the given array of size 1 greater than the argument. + * The last value of the array is left to the default value. + * + * @param array The array to copy, must not be {@code null}. + * @param newArrayComponentType If {@code array} is {@code null}, create a + * size 1 array of this type. + * @return A new copy of the array of size 1 greater than the input. + */ + private static Object copyArrayGrow1(final Object array, final Class newArrayComponentType) { + if (array != null) { + final int arrayLength = Array.getLength(array); + final Object newArray = Array.newInstance(array.getClass().getComponentType(), arrayLength + 1); + System.arraycopy(array, 0, newArray, 0, arrayLength); + return newArray; + } + return Array.newInstance(newArrayComponentType, 1); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0, null)      = [null]
+     * ArrayUtils.add(null, 0, "a")       = ["a"]
+     * ArrayUtils.add(["a"], 1, null)     = ["a", null]
+     * ArrayUtils.add(["a"], 1, "b")      = ["a", "b"]
+     * ArrayUtils.add(["a", "b"], 3, "c") = ["a", "b", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index > array.length). + * @throws IllegalArgumentException if both array and element are null + */ + public static T[] add(final T[] array, final int index, final T element) { + Class clss = null; + if (array != null) { + clss = array.getClass().getComponentType(); + } else if (element != null) { + clss = element.getClass(); + } else { + throw new IllegalArgumentException("Array and element cannot both be null"); + } + @SuppressWarnings("unchecked") // the add method creates an array of type clss, which is type T + final T[] newArray = (T[]) add(array, index, element, clss); + return newArray; + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0, true)          = [true]
+     * ArrayUtils.add([true], 0, false)       = [false, true]
+     * ArrayUtils.add([false], 1, true)       = [false, true]
+     * ArrayUtils.add([true, false], 1, true) = [true, true, false]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index > array.length). + */ + public static boolean[] add(final boolean[] array, final int index, final boolean element) { + return (boolean[]) add(array, index, Boolean.valueOf(element), Boolean.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add(null, 0, 'a')            = ['a']
+     * ArrayUtils.add(['a'], 0, 'b')           = ['b', 'a']
+     * ArrayUtils.add(['a', 'b'], 0, 'c')      = ['c', 'a', 'b']
+     * ArrayUtils.add(['a', 'b'], 1, 'k')      = ['a', 'k', 'b']
+     * ArrayUtils.add(['a', 'b', 'c'], 1, 't') = ['a', 't', 'b', 'c']
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static char[] add(final char[] array, final int index, final char element) { + return (char[]) add(array, index, Character.valueOf(element), Character.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 3)      = [2, 6, 3]
+     * ArrayUtils.add([2, 6], 0, 1)      = [1, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static byte[] add(final byte[] array, final int index, final byte element) { + return (byte[]) add(array, index, Byte.valueOf(element), Byte.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 10)     = [2, 6, 10]
+     * ArrayUtils.add([2, 6], 0, -4)     = [-4, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static short[] add(final short[] array, final int index, final short element) { + return (short[]) add(array, index, Short.valueOf(element), Short.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 10)     = [2, 6, 10]
+     * ArrayUtils.add([2, 6], 0, -4)     = [-4, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static int[] add(final int[] array, final int index, final int element) { + return (int[]) add(array, index, Integer.valueOf(element), Integer.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1L], 0, 2L)           = [2L, 1L]
+     * ArrayUtils.add([2L, 6L], 2, 10L)      = [2L, 6L, 10L]
+     * ArrayUtils.add([2L, 6L], 0, -4L)      = [-4L, 2L, 6L]
+     * ArrayUtils.add([2L, 6L, 3L], 2, 1L)   = [2L, 6L, 1L, 3L]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static long[] add(final long[] array, final int index, final long element) { + return (long[]) add(array, index, Long.valueOf(element), Long.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1.1f], 0, 2.2f)               = [2.2f, 1.1f]
+     * ArrayUtils.add([2.3f, 6.4f], 2, 10.5f)        = [2.3f, 6.4f, 10.5f]
+     * ArrayUtils.add([2.6f, 6.7f], 0, -4.8f)        = [-4.8f, 2.6f, 6.7f]
+     * ArrayUtils.add([2.9f, 6.0f, 0.3f], 2, 1.0f)   = [2.9f, 6.0f, 1.0f, 0.3f]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static float[] add(final float[] array, final int index, final float element) { + return (float[]) add(array, index, Float.valueOf(element), Float.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ *

+ *

+     * ArrayUtils.add([1.1], 0, 2.2)              = [2.2, 1.1]
+     * ArrayUtils.add([2.3, 6.4], 2, 10.5)        = [2.3, 6.4, 10.5]
+     * ArrayUtils.add([2.6, 6.7], 0, -4.8)        = [-4.8, 2.6, 6.7]
+     * ArrayUtils.add([2.9, 6.0, 0.3], 2, 1.0)    = [2.9, 6.0, 1.0, 0.3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static double[] add(final double[] array, final int index, final double element) { + return (double[]) add(array, index, Double.valueOf(element), Double.TYPE); + } + + /** + * Underlying implementation of add(array, index, element) methods. + * The last parameter is the class, which may not equal element.getClass + * for primitives. + * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @param clss the type of the element being added + * @return A new array containing the existing elements and the new element + */ + private static Object add(final Object array, final int index, final Object element, final Class clss) { + if (array == null) { + if (index != 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: 0"); + } + final Object joinedArray = Array.newInstance(clss, 1); + Array.set(joinedArray, 0, element); + return joinedArray; + } + final int length = Array.getLength(array); + if (index > length || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + final Object result = Array.newInstance(clss, length + 1); + System.arraycopy(array, 0, result, 0, index); + Array.set(result, index, element); + if (index < length) { + System.arraycopy(array, index, result, index + 1, length - index); + } + return result; + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove(["a"], 0)           = []
+     * ArrayUtils.remove(["a", "b"], 0)      = ["b"]
+     * ArrayUtils.remove(["a", "b"], 1)      = ["a"]
+     * ArrayUtils.remove(["a", "b", "c"], 1) = ["a", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + @SuppressWarnings("unchecked") // remove() always creates an array of the same type as its input + public static T[] remove(final T[] array, final int index) { + return (T[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, "a")            = null
+     * ArrayUtils.removeElement([], "a")              = []
+     * ArrayUtils.removeElement(["a"], "b")           = ["a"]
+     * ArrayUtils.removeElement(["a", "b"], "a")      = ["b"]
+     * ArrayUtils.removeElement(["a", "b", "a"], "a") = ["b", "a"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static T[] removeElement(final T[] array, final Object element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([true], 0)              = []
+     * ArrayUtils.remove([true, false], 0)       = [false]
+     * ArrayUtils.remove([true, false], 1)       = [true]
+     * ArrayUtils.remove([true, true, false], 1) = [true, false]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static boolean[] remove(final boolean[] array, final int index) { + return (boolean[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, true)                = null
+     * ArrayUtils.removeElement([], true)                  = []
+     * ArrayUtils.removeElement([true], false)             = [true]
+     * ArrayUtils.removeElement([true, false], false)      = [true]
+     * ArrayUtils.removeElement([true, false, true], true) = [false, true]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static boolean[] removeElement(final boolean[] array, final boolean element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1], 0)          = []
+     * ArrayUtils.remove([1, 0], 0)       = [0]
+     * ArrayUtils.remove([1, 0], 1)       = [1]
+     * ArrayUtils.remove([1, 0, 1], 1)    = [1, 1]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static byte[] remove(final byte[] array, final int index) { + return (byte[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1)        = null
+     * ArrayUtils.removeElement([], 1)          = []
+     * ArrayUtils.removeElement([1], 0)         = [1]
+     * ArrayUtils.removeElement([1, 0], 0)      = [1]
+     * ArrayUtils.removeElement([1, 0, 1], 1)   = [0, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static byte[] removeElement(final byte[] array, final byte element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove(['a'], 0)           = []
+     * ArrayUtils.remove(['a', 'b'], 0)      = ['b']
+     * ArrayUtils.remove(['a', 'b'], 1)      = ['a']
+     * ArrayUtils.remove(['a', 'b', 'c'], 1) = ['a', 'c']
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static char[] remove(final char[] array, final int index) { + return (char[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 'a')            = null
+     * ArrayUtils.removeElement([], 'a')              = []
+     * ArrayUtils.removeElement(['a'], 'b')           = ['a']
+     * ArrayUtils.removeElement(['a', 'b'], 'a')      = ['b']
+     * ArrayUtils.removeElement(['a', 'b', 'a'], 'a') = ['b', 'a']
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static char[] removeElement(final char[] array, final char element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1.1], 0)           = []
+     * ArrayUtils.remove([2.5, 6.0], 0)      = [6.0]
+     * ArrayUtils.remove([2.5, 6.0], 1)      = [2.5]
+     * ArrayUtils.remove([2.5, 6.0, 3.8], 1) = [2.5, 3.8]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static double[] remove(final double[] array, final int index) { + return (double[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1.1)            = null
+     * ArrayUtils.removeElement([], 1.1)              = []
+     * ArrayUtils.removeElement([1.1], 1.2)           = [1.1]
+     * ArrayUtils.removeElement([1.1, 2.3], 1.1)      = [2.3]
+     * ArrayUtils.removeElement([1.1, 2.3, 1.1], 1.1) = [2.3, 1.1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static double[] removeElement(final double[] array, final double element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1.1], 0)           = []
+     * ArrayUtils.remove([2.5, 6.0], 0)      = [6.0]
+     * ArrayUtils.remove([2.5, 6.0], 1)      = [2.5]
+     * ArrayUtils.remove([2.5, 6.0, 3.8], 1) = [2.5, 3.8]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static float[] remove(final float[] array, final int index) { + return (float[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1.1)            = null
+     * ArrayUtils.removeElement([], 1.1)              = []
+     * ArrayUtils.removeElement([1.1], 1.2)           = [1.1]
+     * ArrayUtils.removeElement([1.1, 2.3], 1.1)      = [2.3]
+     * ArrayUtils.removeElement([1.1, 2.3, 1.1], 1.1) = [2.3, 1.1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static float[] removeElement(final float[] array, final float element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static int[] remove(final int[] array, final int index) { + return (int[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static int[] removeElement(final int[] array, final int element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static long[] remove(final long[] array, final int index) { + return (long[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static long[] removeElement(final long[] array, final long element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static short[] remove(final short[] array, final int index) { + return (short[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ *

+ *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static short[] removeElement(final short[] array, final short element) { + final int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ *

+ *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + private static Object remove(final Object array, final int index) { + final int length = getLength(array); + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + + final Object result = Array.newInstance(array.getClass().getComponentType(), length - 1); + System.arraycopy(array, 0, result, 0, index); + if (index < length - 1) { + System.arraycopy(array, index + 1, result, index, length - index - 1); + } + + return result; + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll(["a", "b", "c"], 0, 2) = ["b"]
+     * ArrayUtils.removeAll(["a", "b", "c"], 1, 2) = ["a"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + @SuppressWarnings("unchecked") // removeAll() always creates an array of the same type as its input + public static T[] removeAll(final T[] array, final int... indices) { + return (T[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static byte[] removeAll(final byte[] array, final int... indices) { + return (byte[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static short[] removeAll(final short[] array, final int... indices) { + return (short[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static int[] removeAll(final int[] array, final int... indices) { + return (int[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static char[] removeAll(final char[] array, final int... indices) { + return (char[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static long[] removeAll(final long[] array, final int... indices) { + return (long[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static float[] removeAll(final float[] array, final int... indices) { + return (float[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static double[] removeAll(final double[] array, final int... indices) { + return (double[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ *

+ *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ *

+ *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ *

+ *

+     * ArrayUtils.removeAll([true, false, true], 0, 2) = [false]
+     * ArrayUtils.removeAll([true, false, true], 1, 2) = [true]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static boolean[] removeAll(final boolean[] array, final int... indices) { + return (boolean[]) removeAll((Object) array, clone(indices)); + } + + /** + * Removes multiple array elements specified by index. + * + * @param array source + * @param indices to remove, WILL BE SORTED--so only clones of user-owned arrays! + * @return new array of same type minus elements specified by unique values of {@code indices} + * @since 3.0.1 + */ + // package protected for access by unit tests + static Object removeAll(final Object array, final int... indices) { + final int length = getLength(array); + int diff = 0; // number of distinct indexes, i.e. number of entries that will be removed + + if (isNotEmpty(indices)) { + Arrays.sort(indices); + + int i = indices.length; + int prevIndex = length; + while (--i >= 0) { + final int index = indices[i]; + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + if (index >= prevIndex) { + continue; + } + diff++; + prevIndex = index; + } + } + final Object result = Array.newInstance(array.getClass().getComponentType(), length - diff); + if (diff < length) { + int end = length; // index just after last copy + int dest = length - diff; // number of entries so far not copied + for (int i = indices.length - 1; i >= 0; i--) { + final int index = indices[i]; + if (end - index > 1) { // same as (cp > 0) + final int cp = end - index - 1; + dest -= cp; + System.arraycopy(array, index + 1, result, dest, cp); + // Afer this copy, we still have room for dest items. + } + end = index; + } + if (end > 0) { + System.arraycopy(array, 0, result, 0, end); + } + } + return result; + } + + /** + * Removes multiple array elements specified by indices. + * + * @param array source + * @param indices to remove + * @return new array of same type minus elements specified by the set bits in {@code indices} + * @since 3.2 + */ + // package protected for access by unit tests + static Object removeAll(final Object array, final BitSet indices) { + final int srcLength = ArrayUtils.getLength(array); + // No need to check maxIndex here, because method only currently called from removeElements() + // which guarantee to generate on;y valid bit entries. +// final int maxIndex = indices.length(); +// if (maxIndex > srcLength) { +// throw new IndexOutOfBoundsException("Index: " + (maxIndex-1) + ", Length: " + srcLength); +// } + final int removals = indices.cardinality(); // true bits are items to remove + final Object result = Array.newInstance(array.getClass().getComponentType(), srcLength - removals); + int srcIndex = 0; + int destIndex = 0; + int count; + int set; + while ((set = indices.nextSetBit(srcIndex)) != -1) { + count = set - srcIndex; + if (count > 0) { + System.arraycopy(array, srcIndex, result, destIndex, count); + destIndex += count; + } + srcIndex = indices.nextClearBit(set); + } + count = srcLength - srcIndex; + if (count > 0) { + System.arraycopy(array, srcIndex, result, destIndex, count); + } + return result; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/BitmapLruCacheHelper.java b/library/src/main/java/com/chwl/library/utils/BitmapLruCacheHelper.java new file mode 100644 index 0000000..9684c3a --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/BitmapLruCacheHelper.java @@ -0,0 +1,112 @@ +package com.chwl.library.utils; + +import android.graphics.Bitmap; +import android.util.Log; + +import androidx.collection.LruCache; + +/** + * @author Rowand jj + * + *ʹ��Lrucache����bitmap�Ĺ����� + */ +public class BitmapLruCacheHelper +{ + private static final String TAG = "BitmapLruCacheHelper"; + private static BitmapLruCacheHelper instance = new BitmapLruCacheHelper(); + LruCache cache = null; + //���� + private BitmapLruCacheHelper() + { + int maxSize = (int) (Runtime.getRuntime().maxMemory() / (float) 8); + cache = new LruCache(maxSize) + { + @Override + protected int sizeOf(String key, Bitmap value) + { + return value.getRowBytes()*value.getHeight(); + } + }; + } + + public static BitmapLruCacheHelper getInstance() + { + return instance; + } + /** + *���뻺�� + * @param key + * @param value + */ + public void addBitmapToMemCache(String key, Bitmap value) + { + if(key == null || value == null) + { + return; + } + if(cache!=null && getBitmapFromMemCache(key)==null) + { + cache.put(key, value); + Log.i(TAG,"put bitmap to lrucache success"); + } + } + + /** + * �ӻ����л�ȡͼƬ + * @param key + * @return + */ + public Bitmap getBitmapFromMemCache(String key) + { + if(key == null) + { + return null; + } + Bitmap bitmap = cache.get(key); + Log.i(TAG,"get bitmap from lrucache,bitmap="+bitmap); + return bitmap; + } + + /** + * ��ָ��bitmap�ӻ������Ƴ� + * @param key + * @return + */ + public Bitmap removeBitmapFromMemCache(String key) + { + if(key == null) + { + return null; + } + return cache.remove(key); + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/src/main/java/com/chwl/library/utils/BlankUtil.java b/library/src/main/java/com/chwl/library/utils/BlankUtil.java new file mode 100644 index 0000000..ab682d8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/BlankUtil.java @@ -0,0 +1,75 @@ +package com.chwl.library.utils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * 判断对象是否为空 + * Created on 2010-5-25 + */ +public class BlankUtil { + + /** + * 判断字符串是否为空 + * @return 空:true 否则:false + */ + public static boolean isBlank(final String str) { + return (str == null) || (str.trim().length() <= 0); + } + + + /** + * 判断字符是否为空 + * @param cha + * @return + */ + public static boolean isBlank(final Character cha){ + return (cha==null) || cha.equals(' '); + } + + /** + * 判断对象是否为空 + */ + public static boolean isBlank(final Object obj) { + return (obj==null); + } + + /** + * 判断数组是否为空 + * @param objs + * @return + */ + public static boolean isBlank(final Object[] objs) { + return (objs == null) || (objs.length <= 0); + } + + /** + * 判断Collectionj是否为空 + * @param obj + * @return + */ + public static boolean isBlank(final Collection obj) { + return (obj == null) || (obj.size() <= 0); + } + + /** + * 判断Set是否为空 + * @param obj + * @return + */ + public static boolean isBlank(final Set obj) { + return (obj == null) || (obj.size() <= 0); + } + + /** + * 判断Map是否为空 + * @param obj + * @return + */ + public static boolean isBlank(final Map obj) { + return (obj == null) || (obj.size() <= 0); + } + + +} diff --git a/library/src/main/java/com/chwl/library/utils/CenterDrawableHelper.java b/library/src/main/java/com/chwl/library/utils/CenterDrawableHelper.java new file mode 100644 index 0000000..a34f028 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/CenterDrawableHelper.java @@ -0,0 +1,58 @@ +package com.chwl.library.utils; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.widget.TextView; + +public final class CenterDrawableHelper { + static final int DRAWABLE_LEFT = 0; + static final int DRAWABLE_TOP = 1; + static final int DRAWABLE_RIGHT = 2; + static final int DRAWABLE_BOTTOM = 3; + + private static void onCenterDraw(TextView view, Canvas canvas, Drawable drawable, int gravity) { + if (drawable == null) return; + int drawablePadding = view.getCompoundDrawablePadding(); + int ratio = 1; + float total; + switch (gravity) { + case Gravity.RIGHT: + ratio = -1; + case Gravity.LEFT: + total = view.getPaint().measureText(view.getText().toString()) + drawable.getIntrinsicWidth() + + drawablePadding + view.getPaddingLeft() + view.getPaddingRight(); + canvas.translate(ratio * (view.getWidth() - total) / 2, 0); + break; + case Gravity.BOTTOM: + ratio = -1; + case Gravity.TOP: + Paint.FontMetrics fontMetrics0 = view.getPaint().getFontMetrics(); + total = fontMetrics0.descent - fontMetrics0.ascent + drawable.getIntrinsicHeight() + + drawablePadding + view.getPaddingTop() + view.getPaddingBottom(); + canvas.translate(0, ratio * (view.getHeight() - total) / 2); + break; + default: + } + } + + public static void preDraw(TextView view, Canvas canvas) { + Drawable[] drawables = view.getCompoundDrawables(); + if (drawables.length > 0) { + if (drawables[DRAWABLE_LEFT] != null) { + view.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); + onCenterDraw(view, canvas, drawables[DRAWABLE_LEFT], Gravity.LEFT); + } else if (drawables[DRAWABLE_TOP] != null) { + view.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP); + onCenterDraw(view, canvas, drawables[DRAWABLE_TOP], Gravity.TOP); + } else if (drawables[DRAWABLE_RIGHT] != null) { + view.setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); + onCenterDraw(view, canvas, drawables[DRAWABLE_RIGHT], Gravity.RIGHT); + } else if (drawables[DRAWABLE_BOTTOM] != null) { + view.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + onCenterDraw(view, canvas, drawables[DRAWABLE_BOTTOM], Gravity.BOTTOM); + } + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/CharSequenceUtils.java b/library/src/main/java/com/chwl/library/utils/CharSequenceUtils.java new file mode 100644 index 0000000..ddd0199 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/CharSequenceUtils.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +/** + *

Operations on {@link CharSequence} that are + * {@code null} safe.

+ * + * @version $Id: CharSequenceUtils.java 1469220 2013-04-18 08:15:47Z bayard $ + * @see CharSequence + * @since 3.0 + */ +public class CharSequenceUtils { + + /** + *

{@code CharSequenceUtils} instances should NOT be constructed in + * standard programming.

+ *

+ *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public CharSequenceUtils() { + super(); + } + + //----------------------------------------------------------------------- + + /** + *

Returns a new {@code CharSequence} that is a subsequence of this + * sequence starting with the {@code char} value at the specified index.

+ *

+ *

This provides the {@code CharSequence} equivalent to {@link String#substring(int)}. + * The length (in {@code char}) of the returned sequence is {@code length() - start}, + * so if {@code start == end} then an empty sequence is returned.

+ * + * @param cs the specified subsequence, null returns null + * @param start the start index, inclusive, valid + * @return a new subsequence, may be null + * @throws IndexOutOfBoundsException if {@code start} is negative or if + * {@code start} is greater than {@code length()} + */ + public static CharSequence subSequence(final CharSequence cs, final int start) { + return cs == null ? null : cs.subSequence(start, cs.length()); + } + + //----------------------------------------------------------------------- + + /** + *

Finds the first index in the {@code CharSequence} that matches the + * specified character.

+ * + * @param cs the {@code CharSequence} to be processed, not null + * @param searchChar the char to be searched for + * @param start the start index, negative starts at the string start + * @return the index where the search char was found, -1 if not found + */ + static int indexOf(final CharSequence cs, final int searchChar, int start) { + if (cs instanceof String) { + return ((String) cs).indexOf(searchChar, start); + } else { + final int sz = cs.length(); + if (start < 0) { + start = 0; + } + for (int i = start; i < sz; i++) { + if (cs.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + } + + /** + * Used by the indexOf(CharSequence methods) as a green implementation of indexOf. + * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the {@code CharSequence} to be searched for + * @param start the start index + * @return the index where the search sequence was found + */ + static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) { + return cs.toString().indexOf(searchChar.toString(), start); +// if (cs instanceof String && searchChar instanceof String) { +// // TODO: Do we assume searchChar is usually relatively small; +// // If so then calling toString() on it is better than reverting to +// // the green implementation in the else block +// return ((String) cs).indexOf((String) searchChar, start); +// } else { +// // TODO: Implement rather than convert to String +// return cs.toString().indexOf(searchChar.toString(), start); +// } + } + + /** + *

Finds the last index in the {@code CharSequence} that matches the + * specified character.

+ * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the char to be searched for + * @param start the start index, negative returns -1, beyond length starts at end + * @return the index where the search char was found, -1 if not found + */ + static int lastIndexOf(final CharSequence cs, final int searchChar, int start) { + if (cs instanceof String) { + return ((String) cs).lastIndexOf(searchChar, start); + } else { + final int sz = cs.length(); + if (start < 0) { + return -1; + } + if (start >= sz) { + start = sz - 1; + } + for (int i = start; i >= 0; --i) { + if (cs.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + } + + /** + * Used by the lastIndexOf(CharSequence methods) as a green implementation of lastIndexOf + * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the {@code CharSequence} to be searched for + * @param start the start index + * @return the index where the search sequence was found + */ + static int lastIndexOf(final CharSequence cs, final CharSequence searchChar, final int start) { + return cs.toString().lastIndexOf(searchChar.toString(), start); +// if (cs instanceof String && searchChar instanceof String) { +// // TODO: Do we assume searchChar is usually relatively small; +// // If so then calling toString() on it is better than reverting to +// // the green implementation in the else block +// return ((String) cs).lastIndexOf((String) searchChar, start); +// } else { +// // TODO: Implement rather than convert to String +// return cs.toString().lastIndexOf(searchChar.toString(), start); +// } + } + + /** + * Green implementation of toCharArray. + * + * @param cs the {@code CharSequence} to be processed + * @return the resulting char array + */ + static char[] toCharArray(final CharSequence cs) { + if (cs instanceof String) { + return ((String) cs).toCharArray(); + } else { + final int sz = cs.length(); + final char[] array = new char[cs.length()]; + for (int i = 0; i < sz; i++) { + array[i] = cs.charAt(i); + } + return array; + } + } + + /** + * Green implementation of regionMatches. + * + * @param cs the {@code CharSequence} to be processed + * @param ignoreCase whether or not to be case insensitive + * @param thisStart the index to start on the {@code cs} CharSequence + * @param substring the {@code CharSequence} to be looked for + * @param start the index to start on the {@code substring} CharSequence + * @param length character length of the region + * @return whether the region matched + */ + static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart, + final CharSequence substring, final int start, final int length) { + if (cs instanceof String && substring instanceof String) { + return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length); + } else { + int index1 = thisStart; + int index2 = start; + int tmpLen = length; + + while (tmpLen-- > 0) { + char c1 = cs.charAt(index1++); + char c2 = substring.charAt(index2++); + + if (c1 == c2) { + continue; + } + + if (!ignoreCase) { + return false; + } + + // The same check as in String.regionMatches(): + if (Character.toUpperCase(c1) != Character.toUpperCase(c2) + && Character.toLowerCase(c1) != Character.toLowerCase(c2)) { + return false; + } + } + + return true; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/CharUtils.java b/library/src/main/java/com/chwl/library/utils/CharUtils.java new file mode 100644 index 0000000..199815b --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/CharUtils.java @@ -0,0 +1,539 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +/** + *

Operations on char primitives and Character objects.

+ * + *

This class tries to handle {@code null} input gracefully. + * An exception will not be thrown for a {@code null} input. + * Each method documents its behaviour in more detail.

+ * + *

#ThreadSafe#

+ * @since 2.1 + * @version $Id: CharUtils.java 1531257 2013-10-11 11:25:28Z britter $ + */ +public class CharUtils { + + private static final String[] CHAR_STRING_ARRAY = new String[128]; + + /** + * {@code \u000a} linefeed LF ('\n'). + * + * @see JLF: Escape Sequences + * for Character and String Literals + * @since 2.2 + */ + public static final char LF = '\n'; + + /** + * {@code \u000d} carriage return CR ('\r'). + * + * @see JLF: Escape Sequences + * for Character and String Literals + * @since 2.2 + */ + public static final char CR = '\r'; + + + static { + for (char c = 0; c < CHAR_STRING_ARRAY.length; c++) { + CHAR_STRING_ARRAY[c] = String.valueOf(c); + } + } + + /** + *

{@code CharUtils} instances should NOT be constructed in standard programming. + * Instead, the class should be used as {@code CharUtils.toString('c');}.

+ * + *

This constructor is public to permit tools that require a JavaBean instance + * to operate.

+ */ + public CharUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to a Character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same Character object each time.

+ * + *
+     *   CharUtils.toCharacterObject(' ')  = ' '
+     *   CharUtils.toCharacterObject('A')  = 'A'
+     * 
+ * + * @deprecated Java 5 introduced {@link Character#valueOf(char)} which caches chars 0 through 127. + * @param ch the character to convert + * @return a Character of the specified character + */ + @Deprecated + public static Character toCharacterObject(final char ch) { + return Character.valueOf(ch); + } + + /** + *

Converts the String to a Character using the first character, returning + * null for empty Strings.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same Character object each time.

+ * + *
+     *   CharUtils.toCharacterObject(null) = null
+     *   CharUtils.toCharacterObject("")   = null
+     *   CharUtils.toCharacterObject("A")  = 'A'
+     *   CharUtils.toCharacterObject("BA") = 'B'
+     * 
+ * + * @param str the character to convert + * @return the Character value of the first letter of the String + */ + public static Character toCharacterObject(final String str) { + if (StringUtils.isEmpty(str)) { + return null; + } + return Character.valueOf(str.charAt(0)); + } + + //----------------------------------------------------------------------- + /** + *

Converts the Character to a char throwing an exception for {@code null}.

+ * + *
+     *   CharUtils.toChar(' ')  = ' '
+     *   CharUtils.toChar('A')  = 'A'
+     *   CharUtils.toChar(null) throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert + * @return the char value of the Character + * @throws IllegalArgumentException if the Character is null + */ + public static char toChar(final Character ch) { + if (ch == null) { + throw new IllegalArgumentException("The Character must not be null"); + } + return ch.charValue(); + } + + /** + *

Converts the Character to a char handling {@code null}.

+ * + *
+     *   CharUtils.toChar(null, 'X') = 'X'
+     *   CharUtils.toChar(' ', 'X')  = ' '
+     *   CharUtils.toChar('A', 'X')  = 'A'
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the value to use if the Character is null + * @return the char value of the Character or the default if null + */ + public static char toChar(final Character ch, final char defaultValue) { + if (ch == null) { + return defaultValue; + } + return ch.charValue(); + } + + //----------------------------------------------------------------------- + /** + *

Converts the String to a char using the first character, throwing + * an exception on empty Strings.

+ * + *
+     *   CharUtils.toChar("A")  = 'A'
+     *   CharUtils.toChar("BA") = 'B'
+     *   CharUtils.toChar(null) throws IllegalArgumentException
+     *   CharUtils.toChar("")   throws IllegalArgumentException
+     * 
+ * + * @param str the character to convert + * @return the char value of the first letter of the String + * @throws IllegalArgumentException if the String is empty + */ + public static char toChar(final String str) { + if (StringUtils.isEmpty(str)) { + throw new IllegalArgumentException("The String must not be empty"); + } + return str.charAt(0); + } + + /** + *

Converts the String to a char using the first character, defaulting + * the value on empty Strings.

+ * + *
+     *   CharUtils.toChar(null, 'X') = 'X'
+     *   CharUtils.toChar("", 'X')   = 'X'
+     *   CharUtils.toChar("A", 'X')  = 'A'
+     *   CharUtils.toChar("BA", 'X') = 'B'
+     * 
+ * + * @param str the character to convert + * @param defaultValue the value to use if the Character is null + * @return the char value of the first letter of the String or the default if null + */ + public static char toChar(final String str, final char defaultValue) { + if (StringUtils.isEmpty(str)) { + return defaultValue; + } + return str.charAt(0); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3')  = 3
+     *   CharUtils.toIntValue('A')  throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert + * @return the int value of the character + * @throws IllegalArgumentException if the character is not ASCII numeric + */ + public static int toIntValue(final char ch) { + if (isAsciiNumeric(ch) == false) { + throw new IllegalArgumentException("The character " + ch + " is not in the range '0' - '9'"); + } + return ch - 48; + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3', -1)  = 3
+     *   CharUtils.toIntValue('A', -1)  = -1
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the default value to use if the character is not numeric + * @return the int value of the character + */ + public static int toIntValue(final char ch, final int defaultValue) { + if (isAsciiNumeric(ch) == false) { + return defaultValue; + } + return ch - 48; + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3')  = 3
+     *   CharUtils.toIntValue(null) throws IllegalArgumentException
+     *   CharUtils.toIntValue('A')  throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert, not null + * @return the int value of the character + * @throws IllegalArgumentException if the Character is not ASCII numeric or is null + */ + public static int toIntValue(final Character ch) { + if (ch == null) { + throw new IllegalArgumentException("The character must not be null"); + } + return toIntValue(ch.charValue()); + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue(null, -1) = -1
+     *   CharUtils.toIntValue('3', -1)  = 3
+     *   CharUtils.toIntValue('A', -1)  = -1
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the default value to use if the character is not numeric + * @return the int value of the character + */ + public static int toIntValue(final Character ch, final int defaultValue) { + if (ch == null) { + return defaultValue; + } + return toIntValue(ch.charValue(), defaultValue); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to a String that contains the one character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same String object each time.

+ * + *
+     *   CharUtils.toString(' ')  = " "
+     *   CharUtils.toString('A')  = "A"
+     * 
+ * + * @param ch the character to convert + * @return a String containing the one specified character + */ + public static String toString(final char ch) { + if (ch < 128) { + return CHAR_STRING_ARRAY[ch]; + } + return new String(new char[] {ch}); + } + + /** + *

Converts the character to a String that contains the one character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same String object each time.

+ * + *

If {@code null} is passed in, {@code null} will be returned.

+ * + *
+     *   CharUtils.toString(null) = null
+     *   CharUtils.toString(' ')  = " "
+     *   CharUtils.toString('A')  = "A"
+     * 
+ * + * @param ch the character to convert + * @return a String containing the one specified character + */ + public static String toString(final Character ch) { + if (ch == null) { + return null; + } + return toString(ch.charValue()); + } + + //-------------------------------------------------------------------------- + /** + *

Converts the string to the Unicode format '\u0020'.

+ * + *

This format is the Java source code format.

+ * + *
+     *   CharUtils.unicodeEscaped(' ') = "\u0020"
+     *   CharUtils.unicodeEscaped('A') = "\u0041"
+     * 
+ * + * @param ch the character to convert + * @return the escaped Unicode string + */ + public static String unicodeEscaped(final char ch) { + if (ch < 0x10) { + return "\\u000" + Integer.toHexString(ch); + } else if (ch < 0x100) { + return "\\u00" + Integer.toHexString(ch); + } else if (ch < 0x1000) { + return "\\u0" + Integer.toHexString(ch); + } + return "\\u" + Integer.toHexString(ch); + } + + /** + *

Converts the string to the Unicode format '\u0020'.

+ * + *

This format is the Java source code format.

+ * + *

If {@code null} is passed in, {@code null} will be returned.

+ * + *
+     *   CharUtils.unicodeEscaped(null) = null
+     *   CharUtils.unicodeEscaped(' ')  = "\u0020"
+     *   CharUtils.unicodeEscaped('A')  = "\u0041"
+     * 
+ * + * @param ch the character to convert, may be null + * @return the escaped Unicode string, null if null input + */ + public static String unicodeEscaped(final Character ch) { + if (ch == null) { + return null; + } + return unicodeEscaped(ch.charValue()); + } + + //-------------------------------------------------------------------------- + /** + *

Checks whether the character is ASCII 7 bit.

+ * + *
+     *   CharUtils.isAscii('a')  = true
+     *   CharUtils.isAscii('A')  = true
+     *   CharUtils.isAscii('3')  = true
+     *   CharUtils.isAscii('-')  = true
+     *   CharUtils.isAscii('\n') = true
+     *   CharUtils.isAscii('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if less than 128 + */ + public static boolean isAscii(final char ch) { + return ch < 128; + } + + /** + *

Checks whether the character is ASCII 7 bit printable.

+ * + *
+     *   CharUtils.isAsciiPrintable('a')  = true
+     *   CharUtils.isAsciiPrintable('A')  = true
+     *   CharUtils.isAsciiPrintable('3')  = true
+     *   CharUtils.isAsciiPrintable('-')  = true
+     *   CharUtils.isAsciiPrintable('\n') = false
+     *   CharUtils.isAsciiPrintable('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 32 and 126 inclusive + */ + public static boolean isAsciiPrintable(final char ch) { + return ch >= 32 && ch < 127; + } + + /** + *

Checks whether the character is ASCII 7 bit control.

+ * + *
+     *   CharUtils.isAsciiControl('a')  = false
+     *   CharUtils.isAsciiControl('A')  = false
+     *   CharUtils.isAsciiControl('3')  = false
+     *   CharUtils.isAsciiControl('-')  = false
+     *   CharUtils.isAsciiControl('\n') = true
+     *   CharUtils.isAsciiControl('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if less than 32 or equals 127 + */ + public static boolean isAsciiControl(final char ch) { + return ch < 32 || ch == 127; + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic.

+ * + *
+     *   CharUtils.isAsciiAlpha('a')  = true
+     *   CharUtils.isAsciiAlpha('A')  = true
+     *   CharUtils.isAsciiAlpha('3')  = false
+     *   CharUtils.isAsciiAlpha('-')  = false
+     *   CharUtils.isAsciiAlpha('\n') = false
+     *   CharUtils.isAsciiAlpha('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 65 and 90 or 97 and 122 inclusive + */ + public static boolean isAsciiAlpha(final char ch) { + return isAsciiAlphaUpper(ch) || isAsciiAlphaLower(ch); + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic upper case.

+ * + *
+     *   CharUtils.isAsciiAlphaUpper('a')  = false
+     *   CharUtils.isAsciiAlphaUpper('A')  = true
+     *   CharUtils.isAsciiAlphaUpper('3')  = false
+     *   CharUtils.isAsciiAlphaUpper('-')  = false
+     *   CharUtils.isAsciiAlphaUpper('\n') = false
+     *   CharUtils.isAsciiAlphaUpper('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 65 and 90 inclusive + */ + public static boolean isAsciiAlphaUpper(final char ch) { + return ch >= 'A' && ch <= 'Z'; + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic lower case.

+ * + *
+     *   CharUtils.isAsciiAlphaLower('a')  = true
+     *   CharUtils.isAsciiAlphaLower('A')  = false
+     *   CharUtils.isAsciiAlphaLower('3')  = false
+     *   CharUtils.isAsciiAlphaLower('-')  = false
+     *   CharUtils.isAsciiAlphaLower('\n') = false
+     *   CharUtils.isAsciiAlphaLower('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 97 and 122 inclusive + */ + public static boolean isAsciiAlphaLower(final char ch) { + return ch >= 'a' && ch <= 'z'; + } + + /** + *

Checks whether the character is ASCII 7 bit numeric.

+ * + *
+     *   CharUtils.isAsciiNumeric('a')  = false
+     *   CharUtils.isAsciiNumeric('A')  = false
+     *   CharUtils.isAsciiNumeric('3')  = true
+     *   CharUtils.isAsciiNumeric('-')  = false
+     *   CharUtils.isAsciiNumeric('\n') = false
+     *   CharUtils.isAsciiNumeric('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 48 and 57 inclusive + */ + public static boolean isAsciiNumeric(final char ch) { + return ch >= '0' && ch <= '9'; + } + + /** + *

Checks whether the character is ASCII 7 bit numeric.

+ * + *
+     *   CharUtils.isAsciiAlphanumeric('a')  = true
+     *   CharUtils.isAsciiAlphanumeric('A')  = true
+     *   CharUtils.isAsciiAlphanumeric('3')  = true
+     *   CharUtils.isAsciiAlphanumeric('-')  = false
+     *   CharUtils.isAsciiAlphanumeric('\n') = false
+     *   CharUtils.isAsciiAlphanumeric('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 48 and 57 or 65 and 90 or 97 and 122 inclusive + */ + public static boolean isAsciiAlphanumeric(final char ch) { + return isAsciiAlpha(ch) || isAsciiNumeric(ch); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/CommonUtils.java b/library/src/main/java/com/chwl/library/utils/CommonUtils.java new file mode 100644 index 0000000..25ac9d7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/CommonUtils.java @@ -0,0 +1,14 @@ +package com.chwl.library.utils; + +public class CommonUtils { + private static long lastClickTime; + public static boolean isFastDoubleClick(long timeLong) { + long time = System.currentTimeMillis(); + long timeD = time - lastClickTime; + if ( 0 < timeD && timeD < timeLong) { + return true; + } + lastClickTime = time; + return false; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/DeviceUtils.java b/library/src/main/java/com/chwl/library/utils/DeviceUtils.java new file mode 100644 index 0000000..e5a347e --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/DeviceUtils.java @@ -0,0 +1,41 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.orhanobut.logger.Logger; + +/** + * 设备信息工具类 + */ +public class DeviceUtils { + private static final String TAG = "DeviceUtils"; + + /** + * 检查指定包名的APP是否已安装了 + * + * @param context 上下文对象 + * @param packageName 包名 + * @return 是否有安装指定包名的APP,true代表已安装,false代表未安装 + */ + public static boolean isAppInstalled(Context context, String packageName) { + if (context == null) { + return false; + } else { + boolean installed = false; + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + if (info != null) { + installed = true; + } + } catch (Exception e) { + Logger.i("isAppInstalled packageName=" + packageName + " "+ e); + } + return installed; + } + } + +} + diff --git a/library/src/main/java/com/chwl/library/utils/DeviceUuidFactory.java b/library/src/main/java/com/chwl/library/utils/DeviceUuidFactory.java new file mode 100644 index 0000000..30be07f --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/DeviceUuidFactory.java @@ -0,0 +1,71 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.Settings; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * Created by chenran on 2017/10/21. + */ + +public class DeviceUuidFactory { + private static final String PREFS_FILE = "device_id.xml"; + private static final String PREFS_DEVICE_ID = "device_id"; + + public static String getDeviceId(Context context) { + UUID uuid; + synchronized (DeviceUuidFactory.class) { + final SharedPreferences prefs = context.getSharedPreferences(PREFS_FILE, 0); + final String id = prefs.getString(PREFS_DEVICE_ID, null); + if (id != null) { + // Use the ids previously computed and stored in the prefs file + uuid = UUID.fromString(id); + } else { + final String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + // Use the Android ID unless it's broken, in which case fallback on deviceId, + // unless it's not available, then fallback on a random number which we store + // to a prefs file + if (!"9774d56d682e549c".equals(androidId)) { + uuid = UUID.nameUUIDFromBytes(androidId.getBytes(StandardCharsets.UTF_8)); + } else { + uuid = UUID.randomUUID(); + } + // Write the value out to the prefs file + prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).apply(); + } + } + if (uuid == null) { + return ""; + } else { + return uuid.toString(); + } + } + + /** + * Returns a unique UUID for the current android device. As with all UUIDs, this unique ID is "very highly likely" + * to be unique across all Android devices. Much more so than ANDROID_ID is. + * + * The UUID is generated by using ANDROID_ID as the base key if appropriate, falling back on + * TelephonyManager.getDeviceID() if ANDROID_ID is known to be incorrect, and finally falling back + * on a random UUID that's persisted to SharedPreferences if getDeviceID() does not return a + * usable value. + * + * In some rare circumstances, this ID may change. In particular, if the device is factory reset a new device ID + * may be generated. In addition, if a user upgrades their phone from certain buggy implementations of Android 2.2 + * to a newer, non-buggy version of Android, the device ID may change. Or, if a user uninstalls your app on + * a device that has neither a proper Android ID nor a Device ID, this ID may change on reinstallation. + * + * Note that if the code falls back on using TelephonyManager.getDeviceId(), the resulting ID will NOT + * change after a factory reset. Something to be aware of. + * + * Works around a bug in Android 2.2 for many devices when using ANDROID_ID directly. + * + * @see http://code.google.com/p/android/issues/detail?id=10603 + * + * @return a UUID that may be used to uniquely identify your device for most purposes. + */ + +} diff --git a/library/src/main/java/com/chwl/library/utils/FP.java b/library/src/main/java/com/chwl/library/utils/FP.java new file mode 100644 index 0000000..a5b7670 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/FP.java @@ -0,0 +1,909 @@ +package com.chwl.library.utils; + +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.concurrent.Callable; + +/** + * A functional programming style utility + */ +public class FP { + + /* + * Generic function object + */ + + public static Tuple makeTuple(A a, B b, C c) { + return new Tuple(a, b, c); + } + + public static Pred negate(final Pred p) { + return new Pred() { + @Override + public boolean pred(E x) { + return !p.pred(x); + } + }; + } + + public static int limit(int x, int low, int high) { + return Math.min(Math.max(low, x), high); + } + + public static int maximum(int... xs) { + int m = Integer.MIN_VALUE; + for (int x : xs) + m = Math.max(m, x); + return m; + } + + /** + * Find among a list by using predicate function p + */ + public static E find(Pred p, List xs) { + if (!empty(xs)) + for (E x : xs) + if (p.pred(x)) + return x; + return null; + } + + /** + * Find among a list for an element + */ + public static E find(final E x, List xs) { + return find(new Pred() { + @Override + public boolean pred(E y) { + return y.equals(x); + } + }, xs); + } + + public static int findIndex(Pred p, List xs) { + int i, n = length(xs); + for (i = 0; i < n && !p.pred(xs.get(i)); ++i) { + // empty + } + return i == n ? -1 : i; + } + + /** + * looks up a key in an association list (which is realized by pair) + */ + public static V lookup(K k, List> xs) { + if (!empty(xs)) { + for (Pair x : xs) { + if (k == x.first) { + return x.second; + } + } + } + return null; + } + + public static E lookup(int k, SparseArray xs) { + return FP.empty(xs) ? null : xs.get(k); + } + + /* + * Functions over abstract list + */ + + /** + * Remove duplicated items by using a compare function, O(N^2) time bound + */ + public static List nubBy(final Eq cmp, List xs) { + List ys = new ArrayList(); + if (!empty(xs)) { + for (final E x : xs) { + if (find(new Pred() { + @Override + public boolean pred(E y) { + return cmp.eq(x, y); + } + }, ys) == null) { + ys.add(x); + } + } + } + return ys; + } + + /** + * Remove duplicate items by using Object.equals(). O(N^2) time bound + * The name nub means `essence'. + */ + public static List nub(List xs) { + return nubBy(new Eq() { + @Override + public boolean eq(E x, E y) { + return y.equals(x); + } + }, xs); + } + + /** + * Test if a collection is either NIL or empty + */ + public static boolean empty(Collection xs) { + return xs == null || xs.isEmpty(); + } + + /** + * Test if an array is either NIL or empty + */ + public static boolean empty(T[] xs) { + return xs == null || xs.length == 0; + } + + public static boolean empty(SparseArray xs) { + return xs == null || xs.size() == 0; + } + + public static boolean empty(SparseIntArray xs) { + return xs == null || xs.size() == 0; + } + + public static boolean empty(int[] xs) { + return xs == null || xs.length == 0; + } + + /* + * Auxiliary functions + */ + + public static boolean empty(long[] xs) { + return xs == null || xs.length == 0; + } + + /** + * Test if a abstract string is either NIL or empty + */ + public static boolean empty(CharSequence s) { + return s == null || s.length() == 0; + } + + public static boolean empty(Map m) { + return m == null || m.isEmpty(); + } + + /** + * Safe return the size of a collection even it's NIL + */ + public static int size(Collection xs) { + return xs == null ? 0 : xs.size(); + } + + public static int size(CharSequence s) { + return s == null ? 0 : s.length(); + } + + public static int size(T[] xs) { + return xs == null ? 0 : xs.length; + } + + public static int size(int[] xs) { + return xs == null ? 0 : xs.length; + } + + public static int size(long[] xs) { + return xs == null ? 0 : xs.length; + } + + public static int size(Map m) { + return m == null ? 0 : m.size(); + } + + public static int size(SparseArray xs) { + return xs == null ? 0 : xs.size(); + } + + public static int size(SparseIntArray xs) { + return xs == null ? 0 : xs.size(); + } + + /** + * Defined just as alias of 'size'. + */ + public static int length(Collection xs) { + return size(xs); + } + + public static int length(CharSequence s) { + return size(s); + } + + public static int length(T[] xs) { + return size(xs); + } + + public static int length(int[] xs) { + return size(xs); + } + + public static int length(Map m) { + return size(m); + } + + public static int length(SparseArray xs) { + return size(xs); + } + + public static int length(SparseIntArray xs) { + return size(xs); + } + + public static boolean elem(T x, T[] xs) { + return !empty(xs) && Arrays.asList(xs).contains(x); + } + + public static boolean elem(T x, Collection xs) { + return !empty(xs) && xs.contains(x); + } + + public static void swap(List xs, int i, int j) { + T tmp = xs.get(i); + xs.set(i, xs.get(j)); + xs.set(j, tmp); + } + + public static void swap(T[] xs, int i, int j) { + T tmp = xs[i]; + xs[i] = xs[j]; + xs[j] = tmp; + } + + public static void shift(List xs, int from, int to) { + T tmp = xs.get(from); + for (int d = from < to ? 1 : -1; from != to; from += d) { + xs.set(from, xs.get(from + d)); + } + xs.set(to, tmp); + } + + public static void shift(T[] xs, int from, int to) { + T tmp = xs[from]; + for (int d = from < to ? 1 : -1; from != to; from += d) { + xs[from] = xs[from + d]; + } + xs[to] = tmp; + } + + /** + * Safe add element to a list even the list is empty + */ + public static List add(List xs, E x) { + if (xs == null) { + xs = new ArrayList(); + } + xs.add(x); + return xs; + } + + /** + * Safe remove the first occurrent of x in xs + */ + public static List delBy(final Eq cmp, List xs, E x) { + int i, n = FP.length(xs); + for (i = 0; i < n && !cmp.eq(xs.get(i), x); ++i) { + // empty + } + if (i < n) { + xs.remove(i); + } + return xs; + } + + public static List del(List xs, E x) { + return delBy(new Eq() { + @Override + public boolean eq(E x, E y) { + return FP.eq(x, y); + } + }, xs, x); + } + + /** + * span, applied to a predicate p and a list xs, returns a tuple where first + * element is longest prefix (possibly empty) of xs of elements that satisfy + * p and second element is the remainder of the list. + */ + public static Pair, List> span(Pred p, List xs) { + return Pair.create(takeWhile(p, xs), dropWhile(p, xs)); + } + + /** + * Safe take the first n elements from a list + */ + public static List take(int n, List xs) { + List ys = new ArrayList(); + if (empty(xs) || n <= 0) { + return ys; + } + ys.addAll(xs.subList(0, Math.min(n, length(xs)))); + return ys; + } + + public static String take(int n, String s) { + return s.substring(0, limit(n, 0, FP.length(s))); + } + + public static Map take(int n, Map xs) { + Map ys = new HashMap(); + for (Entry k : xs.entrySet()) { + if (n-- > 0) { + ys.put(k.getKey(), k.getValue()); + } + } + return ys; + } + + public static List takeWhile(Pred p, List xs) { + int i, n = length(xs); + for (i = 0; i < n && p.pred(xs.get(i)); ++i) { + // empty + } + return take(i, xs); + } + + /** + * Safe drop the first n elements from a list + */ + public static List drop(int n, List xs) { + List ys = new ArrayList(); + if (xs == null || n > length(xs)) { + return ys; + } + ys.addAll(xs.subList(Math.max(0, n), length(xs))); + return ys; + } + + public static String drop(int n, String s) { + if (s == null || n > length(s)) { + return ""; + } + return s.substring(Math.max(0, n)); + } + + public static List dropWhile(Pred p, List xs) { + int i, n = length(xs); + for (i = 0; i < n && p.pred(xs.get(i)); ++i) { + // empty + } + return drop(n, xs); + } + + public static E head(LinkedList xs) { + return empty(xs) ? null : xs.element(); + } + + public static LinkedList tail(LinkedList xs) { + if (empty(xs)) { + return xs; + } + LinkedList ys = new LinkedList(xs); + ys.remove(); + return ys; + } + + public static LinkedList cons(E x, LinkedList xs) { + xs = empty(xs) ? new LinkedList() : xs; + xs.addFirst(x); + return xs; + } + + public static E first(List xs) { + return FP.empty(xs) ? null : xs.get(0); + } + + public static E second(List xs) { + return FP.size(xs) < 2 ? null : xs.get(1); + } + + /** + * Access the last element of a list + */ + public static E last(List xs) { + return FP.empty(xs) ? null : xs.get(FP.lastIndex(xs)); + } + + /** + * Safe return the index of the last element even if the list is NIL + * + * @return -1 if the list is NIL + */ + public static int lastIndex(List xs) { + return FP.empty(xs) ? -1 : xs.size() - 1; + } + + public static E first(Collection xs) { + if (empty(xs)) { + return null; + } + return xs.iterator().next(); + } + + /** + * Safe convert a collection to list even it's NIL + */ + public static List toList(Collection xs) { + return empty(xs) ? new ArrayList() : new ArrayList(xs); + } + + public static List toList(T x) { + return Collections.singletonList(x); + } + + /** + * Safe convert an array to list even it's NIL + */ + public static List toList(T[] xs) { + List ys = new ArrayList(); + if (!empty(xs)) { + for (T x : xs) { + ys.add(x); + } + } + return ys; + } + + public static List toList(int[] xs) { + List ys = new ArrayList(); + if (!empty(xs)) { + for (int x : xs) { + ys.add(x); + } + } + return ys; + } + + public static List toList(long[] xs) { + List ys = new ArrayList(); + if (!empty(xs)) { + for (long x : xs) { + ys.add(x); + } + } + return ys; + } + + public static List> toList(SparseArray xs) { + List> ys = new ArrayList>(); + if (!empty(xs)) { + for (int i = 0; i < xs.size(); ++i) { + ys.add(Pair.create(xs.keyAt(i), xs.valueAt(i))); + } + } + return ys; + } + + public static List> toList(SparseIntArray xs) { + List> ys = new ArrayList>(); + if (!empty(xs)) { + for (int i = 0; i < xs.size(); ++i) { + ys.add(Pair.create(xs.keyAt(i), xs.valueAt(i))); + } + } + return ys; + } + + public static int[] toArray(List xs) { + int n = length(xs); + int[] ys = new int[n]; + for (int i = 0; i < n; ++i) { + ys[i] = xs.get(i); + } + return ys; + } + + public static List toIntegerList(List list) { + if (list == null) { + return null; + } + List intList = new ArrayList(); + for (Long value : list) { + intList.add(value.intValue()); + } + return intList; + } + + public static List toLongList(List list) { + if (list == null) { + return null; + } + List intList = new ArrayList(); + for (Integer value : list) { + intList.add(value.longValue()); + } + return intList; + } + + /** + * Safe reference to a list even it's NIL + * + * @return reference to the list if it's not NIL, or an empty list. + */ + public static List ref(List xs) { + return xs == null ? new ArrayList() : xs; + } + + @SuppressWarnings({"unchecked"}) + public static E[] ref(E[] xs) { + return xs == null ? (E[]) new Object[]{} : xs; + } + + public static int[] ref(int[] xs) { + return xs == null ? new int[]{} : xs; + } + + public static String ref(String s) { + return s == null ? "" : s; + } + + /** + * Safe zipper + */ + public static List> zip(List as, List bs) { + List> xs = new ArrayList>(); + if (!empty(as) && !empty(bs)) { + Iterator a = as.iterator(); + Iterator b = bs.iterator(); + while (a.hasNext() && b.hasNext()) { + xs.add(Pair.create(a.next(), b.next())); + } + } + return xs; + } + + /** + * Safe equal predicate + */ + public static boolean eq(Object a, Object b) { + if (a == null && b == null) { + return true; + } else if (a == null) { + return false; + } + return a.equals(b); + } + + public static boolean isPrefixOf(String prefix, String s) { + if (empty(prefix)) { + return true; + } + if (empty(s)) { + return false; + } + return s.startsWith(prefix); + } + + public static boolean isPrefixOf(List prefix, List xs) { + if (empty(prefix)) { + return true; + } + if (empty(xs)) { + return false; + } + return eq(prefix, take(length(prefix), xs)); + } + + @SuppressWarnings("unchecked") + public static void convert(T[] dst, Object[] src) { + for (int i = 0; i < src.length; i++) { + dst[i] = (T) src; + } + } + + /** + * Safe concatenation + * Note that the first list is MUTATED if it isn't empty. + */ + public static List concat(List xs, List ys) { + List zs = ref(xs); + zs.addAll(ref(ys)); + return zs; + } + + @SuppressWarnings("unchecked") + public static T[] concat(T[] xs, T[] ys) { + T[] zs = (T[]) new Object[length(xs) + length(ys)]; + int i = 0; + for (T x : xs) { + zs[i++] = x; + } + for (T y : ys) { + zs[i++] = y; + } + return zs; + } + + public static int[] concat(int[] xs, int[] ys) { + int[] zs = new int[length(xs) + length(ys)]; + int i = 0; + for (int x : xs) { + zs[i++] = x; + } + for (int y : ys) { + zs[i++] = y; + } + return zs; + } + + /** + * Safe union, O(N^2) time bound + * Note that + * 1. the first list is mutated if it isn't empty. + * 2. Only the duplicated elements in the second list is removed, those duplicated ones in first is kept. + * If the order needn't be kept, we strongly recommend to use set instead! + */ + public static List unionBy(final Eq cmp, List xs, List ys) { + ys = ref(ys); + if (empty(xs)) { + return ys; + } + for (T y : ys) { + boolean e = false; + for (T x : xs) { + if (cmp.eq(x, y)) { + e = true; + break; + } + } + if (!e) { + xs.add(y); + } + } + return xs; + } + + public static List union(List xs, List ys) { + return unionBy(new Eq() { + @Override + public boolean eq(T x, T y) { + return FP.eq(x, y); + } + }, xs, ys); + } + + /** + * list difference xs \\ ys + * invariant: (xs ++ ys) \\ xs == ys + */ + public static List diffBy(final Eq eq, List xs, List ys) { + List zs = toList(xs); + for (T y : ys) { + zs = delBy(eq, zs, y); + } + return zs; + } + + public static List diff(List xs, List ys) { + return diffBy(new Eq() { + @Override + public boolean eq(T x, T y) { + return FP.eq(x, y); + } + }, xs, ys); + } + + /** + * mapping + */ + public static List map(UnaryFunc f, List xs) { + List ys = new ArrayList(); + for (A x : ref(xs)) { + ys.add(f.apply(x)); + } + return ys; + } + + /** + * filtering + */ + public static List filter(Pred p, List xs) { + List ys = new ArrayList(); + for (E x : xs) { + if (p.pred(x)) { + ys.add(x); + } + } + return ys; + } + + /** + * folding left + */ + public static S fold(BinaryFunc f, S s, Collection xs) { + if (!empty(xs)) { + for (E x : xs) { + s = f.apply(s, x); + } + } + return s; + } + + /** + * ordered insertion. + * O(\lg N) algorithm as it uses binary search + * Please ensure the list support random access, so that the binary search make sense. + */ + public static List insert(Comparator cmp, E x, List xs) { + int pos = Collections.binarySearch(xs, x, cmp); + pos = (pos < 0) ? -pos - 1 : pos; + xs.add(-pos - 1, x); + return xs; + } + + /** + * A wrapper to java.util.Collections.sort(). + * for chained style usage. + */ + public static List sort(Comparator cmp, List xs) { + xs = ref(xs); + try { + Collections.sort(xs, cmp); + } catch (Exception e) { + //YLog.error(FP.class, "Failed to sort %s for %s", xs, e); + } + return xs; + } + + public static int sum(Integer[] xs) { + int n = 0; + for (int x : xs) { + n += x; + } + return n; + } + + public static long sum(Long[] xs) { + long n = 0; + for (long x : xs) { + n += x; + } + return n; + } + + public static int sum(List xs) { + int n = 0; + for (int x : xs) { + n += x; + } + return n; + } + + public static long sum(List xs, Long a) { + long n = 0; + for (long x : xs) { + n += x; + } + return n; + } + + public static int ord(boolean x) { + return x ? 1 : 0; + } + + public static int ord(Integer x) { + return x == null ? 0 : x; + } + + public static List replicate(int n, E x) { + List xs = new ArrayList(); + while (n-- > 0) { + xs.add(x); + } + return xs; + } + + public static List replicate(int n, Callable gen) { + List xs = new ArrayList(); + try { + while (n-- > 0) { + xs.add(gen.call()); + } + } catch (Exception e) { + } + return xs; + } + + public static interface UnaryFunc { + R apply(A x); + } + + public static interface BinaryFunc { + R apply(A a, B b); + } + + /* Predicate */ + public static abstract class Pred implements UnaryFunc { + @Override + public Boolean apply(A x) { + return pred(x); + } + + public abstract boolean pred(A x); + } + + /* Equality */ + public static abstract class Eq implements BinaryFunc { + @Override + public Boolean apply(A x, A y) { + return eq(x, y); + } + + public abstract boolean eq(A x, A y); + } + + public static class Tuple { + public A a; + public B b; + public C c; + + public Tuple(A x, B y, C z) { + a = x; + b = y; + c = z; + } + } + + /** + * Tree based Map (ordable) + */ + public static class M { + public static List> toList(Map m) { + List> xs = new ArrayList>(); + if (!empty(m)) { + for (Entry e : m.entrySet()) { + xs.add(Pair.create(e.getKey(), e.getValue())); + } + } + return xs; + } + + + public static , V> Map fromList(List> xs) { + Map m = new TreeMap(); + if (!empty(xs)) { + for (Pair p : xs) { + m.put(p.first, p.second); + } + } + return m; + } + + public static Map fromList(SparseArray xs) { + Map m = new TreeMap(); + if (!empty(xs)) { + for (int i = 0; i < xs.size(); ++i) { + m.put(xs.keyAt(i), xs.valueAt(i)); + } + } + return m; + } + + public static List values(SparseArray m) { + List xs = new ArrayList(); + int i, n = size(m); + for (i = 0; i < n; ++i) { + xs.add(m.valueAt(i)); + } + return xs; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/FormatUtils.java b/library/src/main/java/com/chwl/library/utils/FormatUtils.java new file mode 100644 index 0000000..d024b3a --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/FormatUtils.java @@ -0,0 +1,205 @@ +package com.chwl.library.utils; + + +import com.chwl.library.R; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + *

格式化工具类

+ * + * @author jiahui + * @date 2018/1/9 + */ +public class FormatUtils { + + public static String formatBigDecimal(double bigDecimal) { + try { + DecimalFormat decimalFormat = new DecimalFormat("#,##0.##"); + decimalFormat.setRoundingMode(RoundingMode.HALF_UP); + decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return decimalFormat.format(bigDecimal); + } catch (Exception e) { + } + return "0.00"; + } + + public static String formatBigInteger(double bigDecimal) { + try { + DecimalFormat decimalFormat = new DecimalFormat("#,##0"); + decimalFormat.setRoundingMode(RoundingMode.HALF_UP); + decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return decimalFormat.format(bigDecimal); + } catch (Exception e) { + } + return "0"; + } + + /** + * 把过长的金额类数字,转换成两位小数带万,亿,兆 缩写 + * 10000.00 -> 1.00万 丢掉两位小数后面的小数 + * + * @param num + * @return + */ + public static String formatToShortDown(double num) { + return formatToShort(num, RoundingMode.DOWN); + } + + /** + * 把过长的金额类数字,转换成两位小数带万,亿,兆 缩写 + * 10000.00 -> 1.00万 四舍五入两位小数后面的小数 + * + * @param num + * @return + */ + public static String formatToShortHalfUp(double num) { + return formatToShort(num, RoundingMode.HALF_UP); + } + + /** + * 把过长的金额类数字,转换成两位小数带万,亿,兆 缩写 + * 10000.00 -> 1.00万 + * + * @param num + * @return + */ + public static String formatToShort(double num, RoundingMode roundingMode) { + try { + DecimalFormat decimalFormat = new DecimalFormat("#,##0.00"); + decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + decimalFormat.setRoundingMode(roundingMode); + double wan = Math.pow(10.0f, 4); + double yi = Math.pow(10.0f, 8); + double zhao = Math.pow(10.0f, 12); + double res = num; + if (Math.abs(num / wan) < 1) { + res = num; + return decimalFormat.format(res); + } else if (Math.abs(num / wan) >= 1 && Math.abs(num / yi) < 1) { + res = num / wan; + return decimalFormat.format(res) + ResUtil.getString(R.string.xchat_android_library_utils_formatutils_01); + } else if (Math.abs(num / yi) >= 1 && Math.abs(num / zhao) < 1) { + res = num / yi; + return decimalFormat.format(res) + ResUtil.getString(R.string.xchat_android_library_utils_formatutils_02); + } else { + res = num / zhao; + return decimalFormat.format(res) + ResUtil.getString(R.string.xchat_android_library_utils_formatutils_03); + } + } catch (Exception e) { + return ResUtil.getString(R.string.xchat_android_library_utils_formatutils_04); + } + } + + /** + * 将手机号码 显示成带隐私形式 + * + * @param phoneNum + * @param beginIndex 开始用* 号替换位置 + * @param endIndex 结束用* 号替换位置 + * @return + */ + public static String formatPhoneNumWithPrivacy(String phoneNum, int beginIndex, int endIndex) { + StringBuffer resSB = new StringBuffer(); + for (int i = 0; i < phoneNum.length(); i++) { + if (i >= beginIndex && i < endIndex) { + resSB.append("*"); + } else { + resSB.append(phoneNum.charAt(i)); + } + } + return resSB.toString(); + } + + //PK值相关格式化方式,统一方法,方便以后更改样式 + public static String formatPKValue(long value) { + // if (value >= 1000000) return (value / 100) / 100f + ResUtil.getString(R.string.xchat_android_library_utils_formatutils_05); + return String.valueOf(value); + } + + + public static String formatBigNum(String num) { + if (StringUtils.isEmpty(num)) { + // 数据为空直接返回0 + return "0"; + } + try { + StringBuilder sb = new StringBuilder(); + if (!StringUtils.isNumeric(num)) { + // 如果数据不是数字则直接返回0 + return "0"; + } + + BigDecimal b0 = new BigDecimal("100000"); + BigDecimal b1 = new BigDecimal("10000"); + BigDecimal b3 = new BigDecimal(num); + + String formatedNum = "";//输出结果 + String unit = "";//单位 + + if (b3.compareTo(b0) < 0) { + sb.append(b3); + } else if (b3.compareTo(b0) == 0 || b3.compareTo(b0) > 0) { + formatedNum = String.valueOf(b3.divide(b1)); + unit = "W"; + } + if (!"".equals(formatedNum)) { + int i = formatedNum.indexOf("."); + if (i == -1) { + sb.append(formatedNum).append(unit); + } else { + String data = formatedNum.substring(0, i); + sb.append(data).append(unit); + } + } + if (sb.length() == 0) + return "0"; + return sb.toString(); + + } catch (Exception e) { + e.printStackTrace(); + return num; + } + } + + public static String formatToShortKMHalfUp(double num) { + return formatToShortKM(num, RoundingMode.HALF_UP); + } + + public static String formatToShortKMDown(double num) { + return formatToShortKM(num, RoundingMode.DOWN); + } + + /** + * 把过长的金额类数字,转换成两位小数带K,M 缩写 + * + * @param num + * @return + */ + public static String formatToShortKM(double num, RoundingMode roundingMode) { + try { + DecimalFormat decimalFormat = new DecimalFormat("#.#"); + decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + decimalFormat.setRoundingMode(roundingMode); + double K = Math.pow(10.0f, 3); + double M = Math.pow(10.0f, 6); + double resK = num / K; + if (resK < 1) { + return decimalFormat.format(num); + } else { + double resM = num / M; + if (resM < 1) { + return decimalFormat.format(resK) + "K"; + } else { + return decimalFormat.format(resM) + "M"; + } + } + } catch (Exception e) { + return String.valueOf(num); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/IOUtils.java b/library/src/main/java/com/chwl/library/utils/IOUtils.java new file mode 100644 index 0000000..473698f --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/IOUtils.java @@ -0,0 +1,27 @@ +package com.chwl.library.utils; + +import java.io.Closeable; +import java.io.IOException; + +/** + *

+ * + * @author jiahui + * @date 2018/2/5 + */ +public class IOUtils { + private IOUtils() { + } + + public static void close(Closeable... closeables) { + if (closeables == null || closeables.length == 0) return; + try { + for (Closeable closeable : closeables) { + if (closeable != null) + closeable.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ImeUtil.java b/library/src/main/java/com/chwl/library/utils/ImeUtil.java new file mode 100644 index 0000000..06213c5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ImeUtil.java @@ -0,0 +1,63 @@ +package com.chwl.library.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import com.chwl.library.utils.asynctask.ScheduledTask; + +/** + * Created with IntelliJ IDEA. + * User: xuduo + * Date: 8/15/13 + * Time: 6:33 PM + * To change this template use File | Settings | File Templates. + * this is a UIUtility class + */ +public class ImeUtil { + + public static void hideIME(Activity activity) { + View view = activity.getCurrentFocus(); + if (null != view) + hideIME(activity, view); + } + + public static void hideIME(Activity activity, View v) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + public static void showIME(Activity activity, View view) { + if (null == view) { + view = activity.getCurrentFocus(); + if (null == view) + return; + } + ((InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE)).showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + + public static void showIME(Activity activity, View view, int flag) { + if (null == view) { + view = activity.getCurrentFocus(); + if (null == view) + return; + } + ((InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE)).showSoftInput(view, flag); + } + + public static void showIMEDelay(final Activity activity, final View view, long time) { + ScheduledTask.getInstance().scheduledDelayed(new Runnable() { + public void run() { + showIME(activity, view); + } + }, time); + } + + public static void hideIME(Context context, EditText editText) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/JavaUtil.java b/library/src/main/java/com/chwl/library/utils/JavaUtil.java new file mode 100644 index 0000000..98c9411 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/JavaUtil.java @@ -0,0 +1,255 @@ +package com.chwl.library.utils; + +import android.annotation.SuppressLint; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import com.chwl.library.R; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.List; +import java.util.Locale; + +/** + * 数据类型转换 + * @author hm + */ +public class JavaUtil { + + public static long str2long(String str){ + if(!TextUtils.isEmpty(str)){ + long result = 0; + try { + result = Long.parseLong(str); + } catch (Exception e) { + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_01)); + } + return result; + } + return 0; + } + + public static Integer str2int(String str) { + int result = 0; + if (!TextUtils.isEmpty(str)) { + try { + result = Integer.parseInt(str); + } catch (Exception e) { + //数据转化问题 + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_02)); + } + return result; + } + return 0; + } + + public static Double str2double(String str) { + double result = 0.0; + if (!TextUtils.isEmpty(str)) { + try { + result = Double.parseDouble(str); + } catch (Exception e) { + //数据转化问题 + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_03)); + } + return result; + } + return 0.0; + } + + public static String str2double2len(String str){ + try { + DecimalFormat df = new DecimalFormat("######0.00"); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + String format = df.format(str2double(str)); + return format; + } catch (Exception e) { + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_04)); + } + return null; + } + + public static String str2double0len(String str){ + try { + DecimalFormat df = new DecimalFormat("######0"); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + String format = df.format(str2double(str)); + return format; + } catch (Exception e) { + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_05)); + } + return null; + } + + public static Float str2flaot(String str) { + float result = 0f; + if (!TextUtils.isEmpty(str)) { + try { + result = Float.parseFloat(str); + } catch (Exception e) { + Log.e("JavaUtil",ResUtil.getString(R.string.xchat_android_library_utils_javautil_06)); + } + return result; + } + return 0f; + } + + /** + * float类型保留两位小数 + */ + public static float float2(float num) { + try { + BigDecimal b = new BigDecimal(num); + float f = b.setScale(2, BigDecimal.ROUND_HALF_UP).floatValue(); + return f; + } catch (Exception e) { + } + return 0; + } + + /** + * double类型保留两位小数 + */ + public static double double2(double f) { + BigDecimal b = new BigDecimal(f); + double df = b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); + return df; + } + + /** + * 保留两位小数,不四舍五入 + * @param value + * @return + */ + public static String formatDecimal (double value){ + final DecimalFormat formatter = new DecimalFormat(); + formatter.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + formatter.setMaximumFractionDigits(2); + formatter.setGroupingSize(0); + formatter.setRoundingMode(RoundingMode.FLOOR); + return formatter.format(value); + } + + public static Float getMin(List list) { + Float min = list.get(0); + for (int i = 0; i < list.size(); i++) { + if (min > list.get(i)) + min = list.get(i); + } + return min; + } + + public static Float getMax(List list) { + Float max = list.get(0); + for (int i = 0; i < list.size(); i++) { + if (max < list.get(i)) + max = list.get(i); + } + return max; + } + + /** + * desc:将数组转为64编码 + * + * @return + */ + public static String objToBase64Str(Object obj) { + if (obj == null) { + return null; + } + // 创建字节输出流 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = null; + String base64Str = null; + try { + oos = new ObjectOutputStream(baos); + // 将对象放到OutputStream中 + oos.writeObject(obj); + base64Str = new String(Base64.encode(baos.toByteArray(), 0)); + } catch (IOException e) { + base64Str = null; + e.printStackTrace(); + } finally { + try { + if (baos != null) { + baos.close(); + } + if (oos != null) { + oos.close(); + } + } catch (IOException e) { + base64Str = null; + } + } + return base64Str; + } + + /** + * desc:将64编码的数据转为对象 + * + * @param data + * @return + */ + @SuppressLint("DefaultLocale") + public static Object base64StrToObj(String data) { + Object obj = null; + if (TextUtils.isEmpty(data)) { + return obj; + } + // 读取字节 + byte[] userByte = Base64.decode(data, 0); + // 封装到字节流 + ByteArrayInputStream bais = new ByteArrayInputStream(userByte); + ObjectInputStream ois = null; + try { + // 再次封装 + ois = new ObjectInputStream(bais); + // 读取对象 + obj = ois.readObject(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (bais != null) { + bais.close(); + } + if (ois != null) { + ois.close(); + } + } catch (IOException e) { + obj = null; + } + } + return obj; + } + + public static int weekStr2int(String str) { + int i = 0; + if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_07))) { + i = 1; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_08))) { + i = 2; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_09))) { + i = 3; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_010))) { + i = 4; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_011))) { + i = 5; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_012))) { + i = 6; + } else if (str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_013)) || str.equals(ResUtil.getString(R.string.xchat_android_library_utils_javautil_014))) { + i = 7; + } + return i; + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/ListUtils.java b/library/src/main/java/com/chwl/library/utils/ListUtils.java new file mode 100644 index 0000000..8e887c6 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ListUtils.java @@ -0,0 +1,17 @@ +package com.chwl.library.utils; + +import java.util.List; + +/** + *

+ * Created by Administrator on 2017/11/16. + */ +public class ListUtils { + + public static boolean isListEmpty(List list) { + return (list == null || list.size() == 0); + } + public static boolean isNotEmpty(List list) { + return (list != null && !list.isEmpty()); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/LogCallerUtils.java b/library/src/main/java/com/chwl/library/utils/LogCallerUtils.java new file mode 100644 index 0000000..1fc6aba --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/LogCallerUtils.java @@ -0,0 +1,46 @@ +package com.chwl.library.utils; + +import com.chwl.library.utils.log.MLog; + +/** + * Created by xujiexing on 14-3-19. + */ +public class LogCallerUtils { + public static final int VERBOSE = 0; + public static final int DEBUG = 1; + public static final int INFO = 2; + public static final int WARN = 3; + public static final int ERROR = 4; + private static final String TAG = "LogCallerUtils"; + + public static void logStack(String msg) { + logStack(msg, VERBOSE); + } + + public static void logStack(String msg, int level) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(); + sb.append(msg + ", caller stack = [ "); + for (StackTraceElement e : stackTraceElements) { + sb.append(e.toString() + ", "); + } + String logs = sb.substring(0, sb.length() - 2) + " ]"; + switch (level) { + case DEBUG: + MLog.debug(TAG, logs); + break; + case INFO: + MLog.info(TAG, logs); + break; + case WARN: + MLog.warn(TAG, logs); + break; + case ERROR: + MLog.error(TAG, logs); + break; + default: + MLog.verbose(TAG, logs); + break; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/LogUtil.java b/library/src/main/java/com/chwl/library/utils/LogUtil.java new file mode 100644 index 0000000..953641f --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/LogUtil.java @@ -0,0 +1,18 @@ +package com.chwl.library.utils; + +import android.util.Log; + +import com.chwl.library.utils.config.BasicConfig; + +/** + * create by lvzebiao @2019/11/20 + */ +public class LogUtil { + + public static void print(String msg) { + if (BasicConfig.INSTANCE.isDebuggable()) { + Log.e("mouse_debug", msg); + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/MimeType.java b/library/src/main/java/com/chwl/library/utils/MimeType.java new file mode 100644 index 0000000..6aa516c --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/MimeType.java @@ -0,0 +1,9 @@ +package com.chwl.library.utils; + +/** + * Created by lijun on 2015/7/20. + */ +public class MimeType { + + public static final String APK = "application/vnd.android.package-archive"; +} diff --git a/library/src/main/java/com/chwl/library/utils/NetworkUtils.java b/library/src/main/java/com/chwl/library/utils/NetworkUtils.java new file mode 100644 index 0000000..0006847 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/NetworkUtils.java @@ -0,0 +1,210 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.chwl.library.utils.config.BasicConfig; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.List; + +/** + * Created by xujiexing on 14-6-10. + */ +public class NetworkUtils { + + public static final int NET_INVALID = 0; // 无网络 + public static final int NET_WIFI = 1; + public static final int NET_2G = 2; + public static final int NET_3G = 3; + public static final int NET_LEGACY = 4; // legacy client + public static final int UNKNOW_NETWORK_TYPE = 5; + + public static boolean isNetworkStrictlyAvailable(@Deprecated Context c) { + ConnectivityManager connectivityManager = (ConnectivityManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null) { + Log.e("xuwakao", "isNetworkStrictlyAvailable connectivityManager is NULL"); + return false; + } + NetworkInfo ni = connectivityManager.getActiveNetworkInfo(); + if (ni != null && ni.isAvailable() && ni.isConnected()) { + return true; + } else { + String info = null; + if (ni != null) { + info = "network type = " + ni.getType() + ", " + + (ni.isAvailable() ? "available" : "inavailable") + + ", " + (ni.isConnected() ? "" : "not") + " connected"; + } else { + info = "no active network"; + } + Log.i("network", info); + return false; + } + } + + public static boolean isNetworkAvailable(@Deprecated Context c) { + ConnectivityManager connectivityManager = (ConnectivityManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = connectivityManager.getActiveNetworkInfo(); + if (ni == null) { + return false; + } + return ni.isConnected() + || (ni.isAvailable() && ni.isConnectedOrConnecting()); + } + + public static int getPort(List ports) { + java.util.Random random = new java.util.Random( + System.currentTimeMillis()); + return ports.get(random.nextInt(ports.size())); + } + + public static NetworkInfo getActiveNetwork(Context c) { + ConnectivityManager cm = (ConnectivityManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); + try { + return cm.getActiveNetworkInfo(); + } catch (Exception e) { + Log.e("NetworkUtils", "error on getActiveNetworkInfo " + e.toString()); + } + return null; + } + + /** + * get the type of network + * + * @param c + * @return + */ + public static int getNetworkType(Context c) { + int networkType = UNKNOW_NETWORK_TYPE; + NetworkInfo netInfo = getActiveNetwork(BasicConfig.INSTANCE.getAppContext()); + if (netInfo != null) { + int type = netInfo.getType(); + if (type == ConnectivityManager.TYPE_WIFI + || type == ConnectivityManager.TYPE_WIMAX) { + networkType = NET_WIFI; + } else if (type == ConnectivityManager.TYPE_MOBILE) { + int subType = netInfo.getSubtype(); + if (subType == TelephonyManager.NETWORK_TYPE_1xRTT + || subType == TelephonyManager.NETWORK_TYPE_UMTS + || subType == TelephonyManager.NETWORK_TYPE_EHRPD + || subType == TelephonyManager.NETWORK_TYPE_EVDO_0 + || subType == TelephonyManager.NETWORK_TYPE_EVDO_A + || subType == TelephonyManager.NETWORK_TYPE_EVDO_B + || subType == TelephonyManager.NETWORK_TYPE_HSDPA + || subType == TelephonyManager.NETWORK_TYPE_HSPA + || subType == TelephonyManager.NETWORK_TYPE_HSPAP + || subType == TelephonyManager.NETWORK_TYPE_HSUPA + || subType == TelephonyManager.NETWORK_TYPE_LTE) { + networkType = NET_3G; + } else if (subType == TelephonyManager.NETWORK_TYPE_GPRS + || subType == TelephonyManager.NETWORK_TYPE_CDMA + || subType == TelephonyManager.NETWORK_TYPE_EDGE + || subType == TelephonyManager.NETWORK_TYPE_IDEN) { + networkType = NET_2G; + } + } + } + return networkType; + } + + public static String getNetworkTypeName(Context context) { + int type = getNetworkType(BasicConfig.INSTANCE.getAppContext()); + switch (type) { + case NET_WIFI: + return "WI-FI"; + case NET_2G: + return "2G"; + case NET_3G: + return "3G"; + case NET_INVALID: + case NET_LEGACY: + case UNKNOW_NETWORK_TYPE: + return "UNKNOWN"; + default: + return "4G"; + } + } + + public static String getSimOperator(Context c) { + TelephonyManager tm = (TelephonyManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.TELEPHONY_SERVICE); + return tm.getSimOperator(); + } + + public static String getOperator(Context c) { + String sim = getSimOperator(BasicConfig.INSTANCE.getAppContext()); + if (FP.empty(sim)) + return ChinaOperator.UNKNOWN; + if (sim.startsWith("46003") || sim.startsWith("46005")) { + return ChinaOperator.CTL; + } else if (sim.startsWith("46001") || sim.startsWith("46006")) { + return ChinaOperator.UNICOM; + } else if (sim.startsWith("46000") || sim.startsWith("46002") + || sim.startsWith("46007") || sim.startsWith("46020")) { + return ChinaOperator.CMCC; + } else { + return ChinaOperator.UNKNOWN; + } + } + + public static String getIPAddress(Context c) { + NetworkInfo info = ((ConnectivityManager) BasicConfig.INSTANCE.getAppContext() + .getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); + if (info != null && info.isConnected()) { + if (info.getType() == ConnectivityManager.TYPE_MOBILE) {//当前使用2G/3G/4G网络 + try { + //Enumeration en=NetworkInterface.getNetworkInterfaces(); + 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() && inetAddress instanceof Inet4Address) { + return inetAddress.getHostAddress(); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + + } else if (info.getType() == ConnectivityManager.TYPE_WIFI) {//当前使用无线网络 + WifiManager wifiManager = (WifiManager) BasicConfig.INSTANCE.getAppContext().getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());//得到IPV4地址 + return ipAddress; + } + } else { + //当前无网络连接,请在设置中打开网络 + } + return null; + } + + /** + * 将得到的int类型的IP转换为String类型 + * + * @param ip + * @return + */ + public static String intIP2StringIP(int ip) { + return (ip & 0xFF) + "." + + ((ip >> 8) & 0xFF) + "." + + ((ip >> 16) & 0xFF) + "." + + (ip >> 24 & 0xFF); + } + + public static class ChinaOperator { + public static final String CMCC = "CMCC"; + public static final String CTL = "CTL"; + public static final String UNICOM = "UNICOM"; + public static final String UNKNOWN = "Unknown"; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/NullUtils.java b/library/src/main/java/com/chwl/library/utils/NullUtils.java new file mode 100644 index 0000000..a9f972c --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/NullUtils.java @@ -0,0 +1,23 @@ +package com.chwl.library.utils; + +import com.chwl.library.R; + +/** + *

+ * + * @author jiahui + * @date 2017/12/4 + */ +public class NullUtils { + public static void checkNull(Object o) { + if (o == null) { + throw new IllegalArgumentException(o + ResUtil.getString(R.string.xchat_android_library_utils_nullutils_01)); + } + } + + public static void checkNull(Object o, String errMsg) { + if (o == null) { + throw new IllegalArgumentException(errMsg); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ObjectUtils.java b/library/src/main/java/com/chwl/library/utils/ObjectUtils.java new file mode 100644 index 0000000..dee5ac5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ObjectUtils.java @@ -0,0 +1,837 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.TreeSet; + +/** + *

Operations on {@code Object}.

+ *

+ *

This class tries to handle {@code null} input gracefully. + * An exception will generally not be thrown for a {@code null} input. + * Each method documents its behaviour in more detail.

+ *

+ *

#ThreadSafe#

+ * + * @version $Id: ObjectUtils.java 1583781 2014-04-01 20:54:53Z niallp $ + * @since 1.0 + */ +//@Immutable +public class ObjectUtils { + + /** + *

Singleton used as a {@code null} placeholder where + * {@code null} has another meaning.

+ *

+ *

For example, in a {@code HashMap} the + * {@link java.util.HashMap#get(Object)} method returns + * {@code null} if the {@code Map} contains {@code null} or if there + * is no matching key. The {@code Null} placeholder can be used to + * distinguish between these two cases.

+ *

+ *

Another example is {@code Hashtable}, where {@code null} + * cannot be stored.

+ *

+ *

This instance is Serializable.

+ */ + public static final Null NULL = new Null(); + + /** + *

{@code ObjectUtils} instances should NOT be constructed in + * standard programming. Instead, the static methods on the class should + * be used, such as {@code ObjectUtils.defaultIfNull("a","b");}.

+ *

+ *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public ObjectUtils() { + super(); + } + + // Defaulting + //----------------------------------------------------------------------- + + /** + *

Returns a default value if the object passed is {@code null}.

+ *

+ *

+     * ObjectUtils.defaultIfNull(null, null)      = null
+     * ObjectUtils.defaultIfNull(null, "")        = ""
+     * ObjectUtils.defaultIfNull(null, "zz")      = "zz"
+     * ObjectUtils.defaultIfNull("abc", *)        = "abc"
+     * ObjectUtils.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE
+     * 
+ * + * @param the type of the object + * @param object the {@code Object} to test, may be {@code null} + * @param defaultValue the default value to return, may be {@code null} + * @return {@code object} if it is not {@code null}, defaultValue otherwise + */ + public static T defaultIfNull(final T object, final T defaultValue) { + return object != null ? object : defaultValue; + } + + /** + *

Returns the first value in the array which is not {@code null}. + * If all the values are {@code null} or the array is {@code null} + * or empty then {@code null} is returned.

+ *

+ *

+     * ObjectUtils.firstNonNull(null, null)      = null
+     * ObjectUtils.firstNonNull(null, "")        = ""
+     * ObjectUtils.firstNonNull(null, null, "")  = ""
+     * ObjectUtils.firstNonNull(null, "zz")      = "zz"
+     * ObjectUtils.firstNonNull("abc", *)        = "abc"
+     * ObjectUtils.firstNonNull(null, "xyz", *)  = "xyz"
+     * ObjectUtils.firstNonNull(Boolean.TRUE, *) = Boolean.TRUE
+     * ObjectUtils.firstNonNull()                = null
+     * 
+ * + * @param the component type of the array + * @param values the values to test, may be {@code null} or empty + * @return the first value from {@code values} which is not {@code null}, + * or {@code null} if there are no non-null values + * @since 3.0 + */ + public static T firstNonNull(final T... values) { + if (values != null) { + for (final T val : values) { + if (val != null) { + return val; + } + } + } + return null; + } + + // Null-safe equals/hashCode + //----------------------------------------------------------------------- + + /** + *

Compares two objects for equality, where either one or both + * objects may be {@code null}.

+ *

+ *

+     * ObjectUtils.equals(null, null)                  = true
+     * ObjectUtils.equals(null, "")                    = false
+     * ObjectUtils.equals("", null)                    = false
+     * ObjectUtils.equals("", "")                      = true
+     * ObjectUtils.equals(Boolean.TRUE, null)          = false
+     * ObjectUtils.equals(Boolean.TRUE, "true")        = false
+     * ObjectUtils.equals(Boolean.TRUE, Boolean.TRUE)  = true
+     * ObjectUtils.equals(Boolean.TRUE, Boolean.FALSE) = false
+     * 
+ * + * @param object1 the first object, may be {@code null} + * @param object2 the second object, may be {@code null} + * @return {@code true} if the values of both objects are the same + * @deprecated this method has been replaced by {@code java.util.Objects.equals(Object, Object)} in Java 7 and will + * be removed from future releases. + */ + @Deprecated + public static boolean objectEquals(final Object object1, final Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + } + + /** + *

Compares two objects for inequality, where either one or both + * objects may be {@code null}.

+ *

+ *

+     * ObjectUtils.notEqual(null, null)                  = false
+     * ObjectUtils.notEqual(null, "")                    = true
+     * ObjectUtils.notEqual("", null)                    = true
+     * ObjectUtils.notEqual("", "")                      = false
+     * ObjectUtils.notEqual(Boolean.TRUE, null)          = true
+     * ObjectUtils.notEqual(Boolean.TRUE, "true")        = true
+     * ObjectUtils.notEqual(Boolean.TRUE, Boolean.TRUE)  = false
+     * ObjectUtils.notEqual(Boolean.TRUE, Boolean.FALSE) = true
+     * 
+ * + * @param object1 the first object, may be {@code null} + * @param object2 the second object, may be {@code null} + * @return {@code false} if the values of both objects are the same + */ + public static boolean notEqual(final Object object1, final Object object2) { + return ObjectUtils.objectEquals(object1, object2) == false; + } + + /** + *

Gets the hash code of an object returning zero when the + * object is {@code null}.

+ *

+ *

+     * ObjectUtils.hashCode(null)   = 0
+     * ObjectUtils.hashCode(obj)    = obj.hashCode()
+     * 
+ * + * @param obj the object to obtain the hash code of, may be {@code null} + * @return the hash code of the object, or zero if null + * @since 2.1 + * @deprecated this method has been replaced by {@code java.util.Objects.hashCode(Object)} in Java 7 and will be + * removed in future releases + */ + @Deprecated + public static int hashCode(final Object obj) { + // hashCode(Object) retained for performance, as hash code is often critical + return obj == null ? 0 : obj.hashCode(); + } + + /** + *

Gets the hash code for multiple objects.

+ *

+ *

This allows a hash code to be rapidly calculated for a number of objects. + * The hash code for a single object is the not same as {@link #hashCode(Object)}. + * The hash code for multiple objects is the same as that calculated by an + * {@code ArrayList} containing the specified objects.

+ *

+ *

+     * ObjectUtils.hashCodeMulti()                 = 1
+     * ObjectUtils.hashCodeMulti((Object[]) null)  = 1
+     * ObjectUtils.hashCodeMulti(a)                = 31 + a.hashCode()
+     * ObjectUtils.hashCodeMulti(a,b)              = (31 + a.hashCode()) * 31 + b.hashCode()
+     * ObjectUtils.hashCodeMulti(a,b,c)            = ((31 + a.hashCode()) * 31 + b.hashCode()) * 31 + c.hashCode()
+     * 
+ * + * @param objects the objects to obtain the hash code of, may be {@code null} + * @return the hash code of the objects, or zero if null + * @since 3.0 + * @deprecated this method has been replaced by {@code java.util.Objects.hash(Object...)} in Java 7 an will be + * removed in future releases. + */ + @Deprecated + public static int hashCodeMulti(final Object... objects) { + int hash = 1; + if (objects != null) { + for (final Object object : objects) { + int tmpHash = ObjectUtils.hashCode(object); + hash = hash * 31 + tmpHash; + } + } + return hash; + } + + /** + *

Gets the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will return {@code null}.

+ *

+ *

+     * ObjectUtils.identityToString(null)         = null
+     * ObjectUtils.identityToString("")           = "java.lang.String@1e23"
+     * ObjectUtils.identityToString(Boolean.TRUE) = "java.lang.Boolean@7fa"
+     * 
+ * + * @param object the object to create a toString for, may be + * {@code null} + * @return the default toString text, or {@code null} if + * {@code null} passed in + */ + public static String identityToString(final Object object) { + if (object == null) { + return null; + } + final StringBuilder builder = new StringBuilder(); + identityToString(builder, object); + return builder.toString(); + } + + // Identity ToString + //----------------------------------------------------------------------- + + /** + *

Appends the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will throw a NullPointerException for either of the two parameters.

+ *

+ *

+     * ObjectUtils.identityToString(appendable, "")            = appendable.append("java.lang.String@1e23"
+     * ObjectUtils.identityToString(appendable, Boolean.TRUE)  = appendable.append("java.lang.Boolean@7fa"
+     * ObjectUtils.identityToString(appendable, Boolean.TRUE)  = appendable.append("java.lang.Boolean@7fa")
+     * 
+ * + * @param appendable the appendable to append to + * @param object the object to create a toString for + * @throws IOException if an I/O error occurs + * @since 3.2 + */ + public static void identityToString(final Appendable appendable, final Object object) throws IOException { + if (object == null) { + throw new NullPointerException("Cannot get the toString of a null identity"); + } + appendable.append(object.getClass().getName()) + .append('@') + .append(Integer.toHexString(System.identityHashCode(object))); + } + + /** + *

Appends the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will throw a NullPointerException for either of the two parameters.

+ *

+ *

+     * ObjectUtils.identityToString(buf, "")            = buf.append("java.lang.String@1e23"
+     * ObjectUtils.identityToString(buf, Boolean.TRUE)  = buf.append("java.lang.Boolean@7fa"
+     * ObjectUtils.identityToString(buf, Boolean.TRUE)  = buf.append("java.lang.Boolean@7fa")
+     * 
+ * + * @param buffer the buffer to append to + * @param object the object to create a toString for + * @since 2.4 + */ + public static void identityToString(final StringBuffer buffer, final Object object) { + if (object == null) { + throw new NullPointerException("Cannot get the toString of a null identity"); + } + buffer.append(object.getClass().getName()) + .append('@') + .append(Integer.toHexString(System.identityHashCode(object))); + } + + /** + *

Appends the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will throw a NullPointerException for either of the two parameters.

+ *

+ *

+     * ObjectUtils.identityToString(builder, "")            = builder.append("java.lang.String@1e23"
+     * ObjectUtils.identityToString(builder, Boolean.TRUE)  = builder.append("java.lang.Boolean@7fa"
+     * ObjectUtils.identityToString(builder, Boolean.TRUE)  = builder.append("java.lang.Boolean@7fa")
+     * 
+ * + * @param builder the builder to append to + * @param object the object to create a toString for + * @since 3.2 + */ + public static void identityToString(final StringBuilder builder, final Object object) { + if (object == null) { + throw new NullPointerException("Cannot get the toString of a null identity"); + } + builder.append(object.getClass().getName()) + .append('@') + .append(Integer.toHexString(System.identityHashCode(object))); + } + + /** + *

Gets the {@code toString} of an {@code Object} returning + * an empty string ("") if {@code null} input.

+ *

+ *

+     * ObjectUtils.toString(null)         = ""
+     * ObjectUtils.toString("")           = ""
+     * ObjectUtils.toString("bat")        = "bat"
+     * ObjectUtils.toString(Boolean.TRUE) = "true"
+     * 
+ * + * @param obj the Object to {@code toString}, may be null + * @return the passed in Object's toString, or {@code ""} if {@code null} input + * @see String#valueOf(Object) + * @since 2.0 + * @deprecated this method has been replaced by {@code java.util.Objects.toString(Object)} in Java 7 and will be + * removed in future releases. Note however that said method will return "null" for null references, while this + * method returns and empty String. To preserve behavior use {@code java.util.Objects.toString(myObject, "")} + */ + @Deprecated + public static String toString(final Object obj) { + return obj == null ? "" : obj.toString(); + } + + // ToString + //----------------------------------------------------------------------- + + /** + *

Gets the {@code toString} of an {@code Object} returning + * a specified text if {@code null} input.

+ *

+ *

+     * ObjectUtils.toString(null, null)           = null
+     * ObjectUtils.toString(null, "null")         = "null"
+     * ObjectUtils.toString("", "null")           = ""
+     * ObjectUtils.toString("bat", "null")        = "bat"
+     * ObjectUtils.toString(Boolean.TRUE, "null") = "true"
+     * 
+ * + * @param obj the Object to {@code toString}, may be null + * @param nullStr the String to return if {@code null} input, may be null + * @return the passed in Object's toString, or {@code nullStr} if {@code null} input + * @see String#valueOf(Object) + * @since 2.0 + * @deprecated this method has been replaced by {@code java.util.Objects.toString(Object, String)} in Java 7 and + * will be removed in future releases. + */ + @Deprecated + public static String toString(final Object obj, final String nullStr) { + return obj == null ? nullStr : obj.toString(); + } + + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param values the set of comparable values, may be null + * @return
    + *
  • If any objects are non-null and unequal, the lesser object. + *
  • If all objects are non-null and equal, the first. + *
  • If any of the comparables are null, the lesser of the non-null objects. + *
  • If all the comparables are null, null is returned. + *
+ */ + public static > T min(final T... values) { + T result = null; + if (values != null) { + for (final T value : values) { + if (compare(value, result, true) < 0) { + result = value; + } + } + } + return result; + } + + // Comparable + //----------------------------------------------------------------------- + + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param values the set of comparable values, may be null + * @return
    + *
  • If any objects are non-null and unequal, the greater object. + *
  • If all objects are non-null and equal, the first. + *
  • If any of the comparables are null, the greater of the non-null objects. + *
  • If all the comparables are null, null is returned. + *
+ */ + public static > T max(final T... values) { + T result = null; + if (values != null) { + for (final T value : values) { + if (compare(value, result, false) > 0) { + result = value; + } + } + } + return result; + } + + /** + *

Null safe comparison of Comparables. + * {@code null} is assumed to be less than a non-{@code null} value.

+ * + * @param type of the values processed by this method + * @param c1 the first comparable, may be null + * @param c2 the second comparable, may be null + * @return a negative value if c1 < c2, zero if c1 = c2 + * and a positive value if c1 > c2 + */ + public static > int compare(final T c1, final T c2) { + return compare(c1, c2, false); + } + + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param c1 the first comparable, may be null + * @param c2 the second comparable, may be null + * @param nullGreater if true {@code null} is considered greater + * than a non-{@code null} value or if false {@code null} is + * considered less than a Non-{@code null} value + * @return a negative value if c1 < c2, zero if c1 = c2 + * and a positive value if c1 > c2 + * @see Comparator#compare(Object, Object) + */ + public static > int compare(final T c1, final T c2, final boolean nullGreater) { + if (c1 == c2) { + return 0; + } else if (c1 == null) { + return nullGreater ? 1 : -1; + } else if (c2 == null) { + return nullGreater ? -1 : 1; + } + return c1.compareTo(c2); + } + + /** + * Find the "best guess" middle value among comparables. If there is an even + * number of total values, the lower of the two middle values will be returned. + * + * @param type of values processed by this method + * @param items to compare + * @return T at middle position + * @throws NullPointerException if items is {@code null} + * @throws IllegalArgumentException if items is empty or contains {@code null} values + * @since 3.0.1 + */ + public static > T median(final T... items) { + Validate.notEmpty(items); + Validate.noNullElements(items); + final TreeSet sort = new TreeSet(); + Collections.addAll(sort, items); + @SuppressWarnings("unchecked") //we know all items added were T instances + final + T result = (T) sort.toArray()[(sort.size() - 1) / 2]; + return result; + } + + /** + * Find the "best guess" middle value among comparables. If there is an even + * number of total values, the lower of the two middle values will be returned. + * + * @param type of values processed by this method + * @param comparator to use for comparisons + * @param items to compare + * @return T at middle position + * @throws NullPointerException if items or comparator is {@code null} + * @throws IllegalArgumentException if items is empty or contains {@code null} values + * @since 3.0.1 + */ + public static T median(final Comparator comparator, final T... items) { + Validate.notEmpty(items, "null/empty items"); + Validate.noNullElements(items); + Validate.notNull(comparator, "null comparator"); + final TreeSet sort = new TreeSet(comparator); + Collections.addAll(sort, items); + @SuppressWarnings("unchecked") //we know all items added were T instances + final + T result = (T) sort.toArray()[(sort.size() - 1) / 2]; + return result; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static boolean MAGIC_FLAG = ObjectUtils.CONST(true);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the boolean value to return + * @return the boolean v, unchanged + * @since 3.2 + */ + public static boolean CONST(final boolean v) { + return v; + } + + // Null + //----------------------------------------------------------------------- + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static byte MAGIC_BYTE = ObjectUtils.CONST((byte) 127);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the byte value to return + * @return the byte v, unchanged + * @since 3.2 + */ + public static byte CONST(final byte v) { + return v; + } + + + // Constants (LANG-816): + /* + These methods ensure constants are not inlined by javac. + For example, typically a developer might declare a constant like so: + + public final static int MAGIC_NUMBER = 5; + + Should a different jar file refer to this, and the MAGIC_NUMBER + is changed a later date (e.g., MAGIC_NUMBER = 6), the different jar + file will need to recompile itself. This is because javac + typically inlines the primitive or String constant directly into + the bytecode, and removes the reference to the MAGIC_NUMBER field. + + To help the other jar (so that it does not need to recompile + when constants are changed) the original developer can declare + their constant using one of the CONST() utility methods, instead: + + public final static int MAGIC_NUMBER = CONST(5); + */ + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static byte MAGIC_BYTE = ObjectUtils.CONST_BYTE(127);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the byte literal (as an int) value to return + * @return the byte v, unchanged + * @throws IllegalArgumentException if the value passed to v + * is larger than a byte, that is, smaller than -128 or + * larger than 127. + * @since 3.2 + */ + public static byte CONST_BYTE(final int v) throws IllegalArgumentException { + if (v < Byte.MIN_VALUE || v > Byte.MAX_VALUE) { + throw new IllegalArgumentException("Supplied value must be a valid byte literal between -128 and 127: [" + v + "]"); + } + return (byte) v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static char MAGIC_CHAR = ObjectUtils.CONST('a');
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the char value to return + * @return the char v, unchanged + * @since 3.2 + */ + public static char CONST(final char v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static short MAGIC_SHORT = ObjectUtils.CONST((short) 123);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the short value to return + * @return the short v, unchanged + * @since 3.2 + */ + public static short CONST(final short v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static short MAGIC_SHORT = ObjectUtils.CONST_SHORT(127);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the short literal (as an int) value to return + * @return the byte v, unchanged + * @throws IllegalArgumentException if the value passed to v + * is larger than a short, that is, smaller than -32768 or + * larger than 32767. + * @since 3.2 + */ + public static short CONST_SHORT(final int v) throws IllegalArgumentException { + if (v < Short.MIN_VALUE || v > Short.MAX_VALUE) { + throw new IllegalArgumentException("Supplied value must be a valid byte literal between -32768 and 32767: [" + v + "]"); + } + return (short) v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static int MAGIC_INT = ObjectUtils.CONST(123);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the int value to return + * @return the int v, unchanged + * @since 3.2 + */ + public static int CONST(final int v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static long MAGIC_LONG = ObjectUtils.CONST(123L);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the long value to return + * @return the long v, unchanged + * @since 3.2 + */ + public static long CONST(final long v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static float MAGIC_FLOAT = ObjectUtils.CONST(1.0f);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the float value to return + * @return the float v, unchanged + * @since 3.2 + */ + public static float CONST(final float v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static double MAGIC_DOUBLE = ObjectUtils.CONST(1.0);
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param v the double value to return + * @return the double v, unchanged + * @since 3.2 + */ + public static double CONST(final double v) { + return v; + } + + /** + * This method returns the provided value unchanged. + * This can prevent javac from inlining a constant + * field, e.g., + *

+ *

+     *     public final static String MAGIC_STRING = ObjectUtils.CONST("abc");
+     * 
+ *

+ * This way any jars that refer to this field do not + * have to recompile themselves if the field's value + * changes at some future date. + * + * @param the Object type + * @param v the genericized Object value to return (typically a String). + * @return the genericized Object v, unchanged (typically a String). + * @since 3.2 + */ + public static T CONST(final T v) { + return v; + } + + @Override + public String toString() { + return super.toString(); + } + + /** + *

Class used as a null placeholder where {@code null} + * has another meaning.

+ *

+ *

For example, in a {@code HashMap} the + * {@link java.util.HashMap#get(Object)} method returns + * {@code null} if the {@code Map} contains {@code null} or if there is + * no matching key. The {@code Null} placeholder can be used to distinguish + * between these two cases.

+ *

+ *

Another example is {@code Hashtable}, where {@code null} + * cannot be stored.

+ */ + public static class Null implements Serializable { + /** + * Required for serialization support. Declare serialization compatibility with Commons Lang 1.0 + * + * @see Serializable + */ + private static final long serialVersionUID = 7092611880189329093L; + + /** + * Restricted constructor - singleton. + */ + Null() { + super(); + } + + /** + *

Ensure singleton.

+ * + * @return the singleton value + */ + private Object readResolve() { + return ObjectUtils.NULL; + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/PathHelper.kt b/library/src/main/java/com/chwl/library/utils/PathHelper.kt new file mode 100644 index 0000000..c458dea --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/PathHelper.kt @@ -0,0 +1,147 @@ +package com.chwl.library.utils + + +import com.chwl.library.utils.codec.MD5Utils +import com.example.lib_utils.AppUtils +import com.example.lib_utils.PathUtils + +/** + * Created by Max on 2022/3/14 18:14 + * Desc:路径助手 + */ +object PathHelper { + + /** + * 获取应用外部Cache目录路径(优先外置存储,其次内置存储) + */ + fun getExternalCachePath(): String { + val path = PathUtils.getExternalAppCachePath(AppUtils.getApp()) + if (!path.isNullOrEmpty()) { + // /storage/emulated/0/Android/data/package/cache + return path + } + // /data/data/package/cache + return PathUtils.getInternalAppCachePath(AppUtils.getApp()) + } + + /** + * 获取应用外部Files目录路径(优先外置存储,其次内置存储) + */ + fun getExternalFilesPath(): String { + val path = PathUtils.getExternalAppFilesPath(AppUtils.getApp()) + if (!path.isNullOrEmpty()) { + // /storage/emulated/0/Android/data/package/files + return path + } + // /data/data/package/files + return PathUtils.getInternalAppFilesPath(AppUtils.getApp()) + } + + /** + * 获取应用内置Cache目录路径 + */ + fun getInternalCachePath(): String { + // /data/data/package/cache + return PathUtils.getInternalAppCachePath(AppUtils.getApp()) + } + + /** + * 获取应用内置Files目录路径 + */ + fun getInternalFilesPath(): String { + // /data/data/package/files + return PathUtils.getInternalAppFilesPath(AppUtils.getApp()) + } + + /** + * 获取应用外部临时Cache目录路径 + */ + fun getExternalTempCachePath(): String { + // /storage/emulated/0/Android/data/package/cache/temp + // or + // /data/data/package/cache/temp + return PathUtils.plusPathNotNull(getExternalCachePath(), "temp") + } + + /** + * 获取应用内置临时Cache目录路径 + */ + fun getInternalTempCachePath(): String { + // /data/data/package/cache/temp + return PathUtils.plusPathNotNull(getInternalCachePath(), "temp") + } + + /** + * 获取应用外部下载目录路径 + */ + fun getExternalDownloadPath(): String { + val path = PathUtils.getExternalAppDownloadPath(AppUtils.getApp()) + if (!path.isNullOrEmpty()) { + // /storage/emulated/0/Android/data/package/files/Download + return path + } + // /data/data/package/files/download + return PathUtils.plusPathNotNull(getInternalFilesPath(), "download") + } + + /** + * 获取应用外部下载缓存目录路径 + */ + fun getExternalDownloadCachePath(): String { + // /storage/emulated/0/Android/data/package/cache/download + return PathUtils.plusPathNotNull(getExternalCachePath(), "download") + } + + /** + * 获取应用内部下载目录路径 + */ + fun getInternalDownloadPath(): String { + // /data/data/package/files/download + return PathUtils.plusPathNotNull(getInternalFilesPath(), "download") + } + + /** + * 生成外部下载文件缓存文件路径 + * @param url 资源地址 + */ + fun generateExternalDownloadCacheFilePath(url: String): String { + // /storage/emulated/0/Android/data/package/cache/download/**.** + return PathUtils.plusPathNotNull(getExternalDownloadCachePath(), generateNameByUrl(url)) + } + + /** + * 获取下载文件路径(文件路径) + * @param url 资源地址 + */ + fun generateExternalDownloadFilePath(url: String): String { + // /storage/emulated/0/Android/data/package/files/Download/**.** + return PathUtils.plusPathNotNull(getExternalDownloadPath(), generateNameByUrl(url)) + } + + /** + * 生成外部临时缓存文件路径 + * @param url 资源地址 + */ + fun generateExternalTempCacheFilePath(url: String): String { + // /storage/emulated/0/Android/data/package/cache/temp/**.** + return PathUtils.plusPathNotNull(getExternalTempCachePath(), generateNameByUrl(url)) + } + + /** + * 生成礼物文件存储文件路径 PS:项目中所有的MP4、SVGA都在此维护 + * @param url 礼物地址 + */ + fun generateResourcesFilePath(url: String): String { + val dir = PathUtils.plusPathNotNull(getInternalFilesPath(), "resources") + return PathUtils.plusPathNotNull(dir, generateNameByUrl(url)) + } + + /** + * 根据Url生成名称 + * @param url url + */ + fun generateNameByUrl(url: String): String { + // md5路径+后缀 + return MD5Utils.getMD5String(url) + (PathUtils.getSuffixType(url) ?: "") + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ResUtil.java b/library/src/main/java/com/chwl/library/utils/ResUtil.java new file mode 100644 index 0000000..0cde4ef --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ResUtil.java @@ -0,0 +1,51 @@ +package com.chwl.library.utils; + +import android.app.Application; +import android.content.Context; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; +import androidx.core.util.Supplier; + + +public class ResUtil { + private static Application context; + + public static Supplier contextSupplier; + + public static void init(Application context) { + ResUtil.context = context; + } + + public static String getString(@StringRes int resId) { + if (contextSupplier != null) { + Context context1 = contextSupplier.get(); + if (context1 != null) { + return context1.getString(resId); + } + } + return context.getString(resId); + } + + public static String getString(@StringRes int resId,Object... vars) { + if (contextSupplier != null) { + Context context1 = contextSupplier.get(); + if (context1 != null) { + return context1.getString(resId,vars); + } + } + return context.getString(resId,vars); + } + + public static int getColor(@ColorRes int resId){ + if (contextSupplier != null) { + Context context1 = contextSupplier.get(); + if (context1 != null) { + return ContextCompat.getColor(context1, resId); + } + } + return ContextCompat.getColor(context, resId); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ResolutionUtils.java b/library/src/main/java/com/chwl/library/utils/ResolutionUtils.java new file mode 100644 index 0000000..c473fe5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ResolutionUtils.java @@ -0,0 +1,203 @@ +package com.chwl.library.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Display; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +public class ResolutionUtils { + + /** + * Gets the width of the display, in pixels. + *

+ * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + *

+ * The size is adjusted based on the current rotation of the display. + *

+ * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @return Screen width in pixels. + */ + public static int getScreenWidth(Context context) { + return getScreenSize(context, null).x; + } + + /** + * Gets the height of the display, in pixels. + *

+ * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + *

+ * The size is adjusted based on the current rotation of the display. + *

+ * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @return Screen height in pixels. + */ + public static int getScreenHeight(Context context) { + return getScreenSize(context, null).y; + } + + /** + * Gets the size of the display, in pixels. + *

+ * Note that this value should not be used for computing layouts, since a + * device will typically have screen decoration (such as a status bar) along + * the edges of the display that reduce the amount of application space + * available from the size returned here. Layouts should instead use the + * window size. + *

+ * The size is adjusted based on the current rotation of the display. + *

+ * The size returned by this method does not necessarily represent the + * actual raw size (native resolution) of the display. The returned size may + * be adjusted to exclude certain system decoration elements that are always + * visible. It may also be scaled to provide compatibility with older + * applications that were originally designed for smaller displays. + * + * @param outSize null-ok. If it is null, will create a Point instance inside, + * otherwise use it to fill the output. NOTE if it is not null, + * it will be the returned value. + * @return Screen size in pixels, the x is the width, the y is the height. + */ + public static Point getScreenSize(Context context, Point outSize) { + WindowManager wm = (WindowManager) context + .getSystemService(Context.WINDOW_SERVICE); + Point ret = outSize == null ? new Point() : outSize; + final Display defaultDisplay = wm.getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= 13) { + defaultDisplay.getSize(ret); + } else { + ret.x = defaultDisplay.getWidth(); + ret.y = defaultDisplay.getHeight(); + } + return ret; + } + + /** + * This method converts dp unit to equivalent pixels, depending on device density. + * + * @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels + * @param context Context to get resources and device specific display metrics + * @return A float value to represent px equivalent to dp depending on device density + */ + public static float convertDpToPixel(float dp, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + float px = dp * (metrics.densityDpi / 160f); + return px; + } + + /** + * This method converts device specific pixels to density independent pixels. + * + * @param px A value in px (pixels) unit. Which we need to convert into db + * @param context Context to get resources and device specific display metrics + * @return A float value to represent dp equivalent to px value + */ + public static float convertPixelsToDp(float px, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + float dp = px / (metrics.densityDpi / 160f); + return dp; + } + + public static float getDensity(Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return metrics.density; + } + + public static float getDensityDpi(Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return metrics.densityDpi; + } + +// public static int getStatusBarHeight(Activity activity) { +// int result = 0; +// if (null != activity) { +// Rect rectangle = new Rect(); +// Window window = activity.getWindow(); +// window.getDecorView().getWindowVisibleDisplayFrame(rectangle); +// int statusBarHeight = rectangle.top; +// int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop(); +// result = contentViewTop - statusBarHeight; +// } +// +// return result; +// } + + public static int getStatusBarHeight2(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + public static int getActionBarHeight(Context context) { + int actionBarHeight = 0; + if (null != context) { + TypedValue tv = new TypedValue(); + if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { + actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics()); + } + } + return actionBarHeight; + } + + @TargetApi(14) + public static boolean hasNavigationBar(Context context) { + boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + return (!hasMenuKey && !hasBackKey); + } + + /** + * TextAppearance.StatusBar.EventContent + * + */ + public static int getDeviceDefaultNotificationTextColor(Context context) { + int[] attrs = {android.R.attr.textColor, android.R.attr.textSize}; + TypedArray ta = context.obtainStyledAttributes(android.R.style.TextAppearance_StatusBar_EventContent, attrs); + int notificationTextColor = ta.getColor(0, Color.WHITE); + ta.recycle(); + return notificationTextColor; + } + + public static boolean isDarkColorStyleForNotification(Context context) { + float[] textNsv = new float[3]; + Color.colorToHSV(ResolutionUtils.getDeviceDefaultNotificationTextColor(context), textNsv); + return (textNsv[2] > 0.5); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/SafeDispatchHandler.java b/library/src/main/java/com/chwl/library/utils/SafeDispatchHandler.java new file mode 100644 index 0000000..0b9bca3 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/SafeDispatchHandler.java @@ -0,0 +1,42 @@ +package com.chwl.library.utils; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.chwl.library.utils.log.MLog; + +/** + * 可安全分发任务的Handler + * + * @author zhongyongsheng + */ +public class SafeDispatchHandler extends Handler { + + public SafeDispatchHandler(Looper looper) { + super(looper); + } + + public SafeDispatchHandler(Looper looper, Callback callback) { + super(looper, callback); + } + + public SafeDispatchHandler() { + super(); + } + + public SafeDispatchHandler(Callback callback) { + super(callback); + } + + @Override + public void dispatchMessage(Message msg) { + try { + super.dispatchMessage(msg); + } catch (Exception e) { + MLog.error(this, e.getMessage(), e); + } catch (Error error) { + MLog.error(this, error.getMessage(), error); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/ScreenUtils.java b/library/src/main/java/com/chwl/library/utils/ScreenUtils.java new file mode 100644 index 0000000..a7b968b --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/ScreenUtils.java @@ -0,0 +1,260 @@ +package com.chwl.library.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.Bitmap; +import android.graphics.Point; +import android.os.Build; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +/** + *

+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/08/02
+ *     desc  : 屏幕相关工具类
+ * 
+ */ +public final class ScreenUtils { + + private ScreenUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 获取屏幕的宽度(单位:px) + * + * @return 屏幕宽 + */ + public static int getScreenWidth(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (wm == null) { + return context.getResources().getDisplayMetrics().widthPixels; + } + Point point = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + wm.getDefaultDisplay().getRealSize(point); + } else { + wm.getDefaultDisplay().getSize(point); + } + return point.x; + } + + /** + * 获取屏幕的高度(单位:px) + * + * @return 屏幕高 + */ + public static int getScreenHeight(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (wm == null) { + return context.getResources().getDisplayMetrics().heightPixels; + } + Point point = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + wm.getDefaultDisplay().getRealSize(point); + } else { + wm.getDefaultDisplay().getSize(point); + } + return point.y; + } + + /** + * 获取屏幕密度 + * + * @return 屏幕密度 + */ + public static float getScreenDensity(Context context) { + return context.getResources().getDisplayMetrics().density; + } + + /** + * 获取屏幕密度 DPI + * + * @return 屏幕密度 DPI + */ + public static int getScreenDensityDpi(Context context) { + return context.getResources().getDisplayMetrics().densityDpi; + } + + /** + * 设置屏幕为全屏 + * + * @param activity activity + */ + public static void setFullScreen(@NonNull final Activity activity) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + /** + * 设置屏幕为横屏 + *

还有一种就是在 Activity 中加属性 android:screenOrientation="landscape"

+ *

不设置 Activity 的 android:configChanges 时, + * 切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次

+ *

设置 Activity 的 android:configChanges="orientation"时, + * 切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次

+ *

设置 Activity 的 android:configChanges="orientation|keyboardHidden|screenSize" + * (4.0 以上必须带最后一个参数)时 + * 切屏不会重新调用各个生命周期,只会执行 onConfigurationChanged 方法

+ * + * @param activity activity + */ + public static void setLandscape(@NonNull final Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } + + /** + * 设置屏幕为竖屏 + * + * @param activity activity + */ + public static void setPortrait(@NonNull final Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + /** + * 判断是否横屏 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isLandscape(Context context) { + return context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 判断是否竖屏 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isPortrait(Context context) { + return context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT; + } + + /** + * 获取屏幕旋转角度 + * + * @param activity activity + * @return 屏幕旋转角度 + */ + public static int getScreenRotation(@NonNull final Activity activity) { + switch (activity.getWindowManager().getDefaultDisplay().getRotation()) { + default: + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + } + } + + /** + * 截屏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap screenShot(@NonNull final Activity activity) { + return screenShot(activity, false); + } + + /** + * 截屏 + * + * @param activity activity + * @return Bitmap + */ + public static Bitmap screenShot(@NonNull final Activity activity, boolean isDeleteStatusBar) { + View decorView = activity.getWindow().getDecorView(); + decorView.setDrawingCacheEnabled(true); + decorView.buildDrawingCache(); + Bitmap bmp = decorView.getDrawingCache(); + DisplayMetrics dm = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(dm); + Bitmap ret; + if (isDeleteStatusBar) { + Resources resources = activity.getResources(); + int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); + int statusBarHeight = resources.getDimensionPixelSize(resourceId); + ret = Bitmap.createBitmap( + bmp, + 0, + statusBarHeight, + dm.widthPixels, + dm.heightPixels - statusBarHeight + ); + } else { + ret = Bitmap.createBitmap(bmp, 0, 0, dm.widthPixels, dm.heightPixels); + } + decorView.destroyDrawingCache(); + return ret; + } + + /** + * 判断是否锁屏 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isScreenLock(Context context) { + KeyguardManager km = + (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + return km != null && km.inKeyguardRestrictedInputMode(); + } + + /** + * 设置进入休眠时长 + *

需添加权限 {@code }

+ * + * @param duration 时长 + */ + public static void setSleepDuration(Context context, final int duration) { + Settings.System.putInt( + context.getContentResolver(), + Settings.System.SCREEN_OFF_TIMEOUT, + duration + ); + } + + /** + * 获取进入休眠时长 + * + * @return 进入休眠时长,报错返回-123 + */ + public static int getSleepDuration(Context context) { + try { + return Settings.System.getInt( + context.getContentResolver(), + Settings.System.SCREEN_OFF_TIMEOUT + ); + } catch (Settings.SettingNotFoundException e) { + e.printStackTrace(); + return -123; + } + } + + /** + * 判断是否是平板 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/SingleToastUtil.java b/library/src/main/java/com/chwl/library/utils/SingleToastUtil.java new file mode 100644 index 0000000..21d2cf0 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/SingleToastUtil.java @@ -0,0 +1,51 @@ +package com.chwl.library.utils; + + +import android.content.Context; + +import com.hjq.toast.ToastUtils; + +/** + * Created by qinbo on 2014/8/12. + */ +public class SingleToastUtil { + + public static void showToast(String s) { + ToastUtils.show(s); + } + + public static void showToast(int resId) { + ToastUtils.show(resId); + } + + @Deprecated + public static void showToastShort(String s) { + ToastUtils.show(s); + } + + @Deprecated + public static void showToastShort(int resId) { + ToastUtils.show(resId); + } + + @Deprecated + public static void showToast(Context context, String s) { + ToastUtils.show(s); + } + + @Deprecated + public static void showToast(Context mContext, final String s, final int length) { + ToastUtils.show(s); + } + + @Deprecated + public static void showToast(Context context, int resId, int length) { + ToastUtils.show(resId); + } + + @Deprecated + public static void showToast(Context context, int resId) { + ToastUtils.show(resId); + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/SizeUtils.java b/library/src/main/java/com/chwl/library/utils/SizeUtils.java new file mode 100644 index 0000000..5f8ce54 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/SizeUtils.java @@ -0,0 +1,176 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/08/02
+ *     desc  : 尺寸相关工具类
+ * 
+ */ +public final class SizeUtils { + + private SizeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * dp 转 px + * + * @param dpValue dp 值 + * @return px 值 + */ + public static int dp2px(Context context, final float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * px 转 dp + * + * @param pxValue px 值 + * @return dp 值 + */ + public static int px2dp(Context context, final float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * sp 转 px + * + * @param spValue sp 值 + * @return px 值 + */ + public static int sp2px(Context context, final float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * px 转 sp + * + * @param pxValue px 值 + * @return sp 值 + */ + public static int px2sp(Context context, final float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 各种单位转换 + *

该方法存在于 TypedValue

+ * + * @param unit 单位 + * @param value 值 + * @param metrics DisplayMetrics + * @return 转换结果 + */ + public static float applyDimension(final int unit, + final float value, + final DisplayMetrics metrics) { + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + default: + } + return 0; + } + + /** + * 在 onCreate 中获取视图的尺寸 + *

需回调 onGetSizeListener 接口,在 onGetSize 中获取 view 宽高

+ *

用法示例如下所示

+ *
+     * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
+     *     Override
+     *     public void onGetSize(final View view) {
+     *         view.getWidth();
+     *     }
+     * });
+     * 
+ * + * @param view 视图 + * @param listener 监听器 + */ + public static void forceGetViewSize(final View view, final onGetSizeListener listener) { + view.post(new Runnable() { + @Override + public void run() { + if (listener != null) { + listener.onGetSize(view); + } + } + }); + } + + /** + * 获取到 View 尺寸的监听 + */ + public interface onGetSizeListener { + void onGetSize(View view); + } + + /** + * 测量视图尺寸 + * + * @param view 视图 + * @return arr[0]: 视图宽度, arr[1]: 视图高度 + */ + public static int[] measureView(final View view) { + ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + } + int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width); + int lpHeight = lp.height; + int heightSpec; + if (lpHeight > 0) { + heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()}; + } + + /** + * 获取测量视图宽度 + * + * @param view 视图 + * @return 视图宽度 + */ + public static int getMeasuredWidth(final View view) { + return measureView(view)[0]; + } + + /** + * 获取测量视图高度 + * + * @param view 视图 + * @return 视图高度 + */ + public static int getMeasuredHeight(final View view) { + return measureView(view)[1]; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/StringUtils.java b/library/src/main/java/com/chwl/library/utils/StringUtils.java new file mode 100644 index 0000000..bcf1c35 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/StringUtils.java @@ -0,0 +1,7569 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

Operations on {@link String} that are + * {@code null} safe.

+ *

+ *

    + *
  • IsEmpty/IsBlank + * - checks if a String contains text
  • + *
  • Trim/Strip + * - removes leading and trailing whitespace
  • + *
  • Equals + * - compares two strings null-safe
  • + *
  • startsWith + * - check if a String starts with a prefix null-safe
  • + *
  • endsWith + * - check if a String ends with a suffix null-safe
  • + *
  • IndexOf/LastIndexOf/Contains + * - null-safe index-of checks + *
  • IndexOfAny/LastIndexOfAny/IndexOfAnyBut/LastIndexOfAnyBut + * - index-of any of a set of Strings
  • + *
  • ContainsOnly/ContainsNone/ContainsAny + * - does String contains only/none/any of these characters
  • + *
  • Substring/Left/Right/Mid + * - null-safe substring extractions
  • + *
  • SubstringBefore/SubstringAfter/SubstringBetween + * - substring extraction relative to other strings
  • + *
  • Split/Join + * - splits a String into an array of substrings and vice versa
  • + *
  • Remove/Delete + * - removes part of a String
  • + *
  • Replace/Overlay + * - Searches a String and replaces one String with another
  • + *
  • Chomp/Chop + * - removes the last part of a String
  • + *
  • AppendIfMissing + * - appends a suffix to the end of the String if not present
  • + *
  • PrependIfMissing + * - prepends a prefix to the start of the String if not present
  • + *
  • LeftPad/RightPad/Center/Repeat + * - pads a String
  • + *
  • UpperCase/LowerCase/SwapCase/Capitalize/Uncapitalize + * - changes the case of a String
  • + *
  • CountMatches + * - counts the number of occurrences of one String in another
  • + *
  • IsAlpha/IsNumeric/IsWhitespace/IsAsciiPrintable + * - checks the characters in a String
  • + *
  • DefaultString + * - protects against a null input String
  • + *
  • Reverse/ReverseDelimited + * - reverses a String
  • + *
  • Abbreviate + * - abbreviates a string using ellipsis
  • + *
  • Difference + * - compares Strings and reports on their differences
  • + *
  • LevenshteinDistance + * - the number of changes needed to change one String into another
  • + *
+ *

+ *

The {@code StringUtils} class defines certain words related to + * String handling.

+ *

+ *

    + *
  • null - {@code null}
  • + *
  • empty - a zero-length string ({@code ""})
  • + *
  • space - the space character ({@code ' '}, char 32)
  • + *
  • whitespace - the characters defined by {@link Character#isWhitespace(char)}
  • + *
  • trim - the characters <= 32 as in {@link String#trim()}
  • + *
+ *

+ *

{@code StringUtils} handles {@code null} input Strings quietly. + * That is to say that a {@code null} input will return {@code null}. + * Where a {@code boolean} or {@code int} is being returned + * details vary by method.

+ *

+ *

A side effect of the {@code null} handling is that a + * {@code NullPointerException} should be considered a bug in + * {@code StringUtils}.

+ *

+ *

Methods in this class give sample code to explain their operation. + * The symbol {@code *} is used to indicate any input including {@code null}.

+ *

+ *

#ThreadSafe#

+ * + * @version $Id: StringUtils.java 1583482 2014-03-31 22:54:57Z niallp $ + * @see String + * @since 1.0 + */ +//@Immutable +public class StringUtils { + // Performance testing notes (JDK 1.4, Jul03, scolebourne) + // Whitespace: + // Character.isWhitespace() is faster than WHITESPACE.indexOf() + // where WHITESPACE is a string of all whitespace characters + // + // Character access: + // String.charAt(n) versus toCharArray(), then array[n] + // String.charAt(n) is about 15% worse for a 10K string + // They are about equal for a length 50 string + // String.charAt(n) is about 4 times better for a length 3 string + // String.charAt(n) is best bet overall + // + // Append: + // String.concat about twice as fast as StringBuffer.append + // (not sure who tested this) + + /** + * A String for a space character. + * + * @since 3.2 + */ + public static final String SPACE = " "; + + /** + * The empty String {@code ""}. + * + * @since 2.0 + */ + public static final String EMPTY = ""; + + /** + * A String for linefeed LF ("\n"). + * + * @see
JLF: Escape Sequences + * for Character and String Literals + * @since 3.2 + */ + public static final String LF = "\n"; + + /** + * A String for carriage return CR ("\r"). + * + * @see JLF: Escape Sequences + * for Character and String Literals + * @since 3.2 + */ + public static final String CR = "\r"; + + /** + * Represents a failed index search. + * + * @since 2.1 + */ + public static final int INDEX_NOT_FOUND = -1; + + /** + *

The maximum size to which the padding constant(s) can expand.

+ */ + private static final int PAD_LIMIT = 8192; + + /** + * A regex pattern for recognizing blocks of whitespace characters. + * The apparent convolutedness of the pattern serves the purpose of + * ignoring "blocks" consisting of only a single space: the pattern + * is used only to normalize whitespace, condensing "blocks" down to a + * single space, thus matching the same would likely cause a great + * many noop replacements. + */ + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("(?: |\\u00A0|\\s|[\\s&&[^ ]])\\s*"); + + /** + *

{@code StringUtils} instances should NOT be constructed in + * standard programming. Instead, the class should be used as + * {@code StringUtils.trim(" foo ");}.

+ *

+ *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public StringUtils() { + super(); + } + + // Empty checks + //----------------------------------------------------------------------- + + /** + *

Checks if a CharSequence is empty ("") or null.

+ *

+ *

+     * StringUtils.isEmpty(null)      = true
+     * StringUtils.isEmpty("")        = true
+     * StringUtils.isEmpty(" ")       = false
+     * StringUtils.isEmpty("bob")     = false
+     * StringUtils.isEmpty("  bob  ") = false
+     * 
+ *

+ *

NOTE: This method changed in Lang version 2.0. + * It no longer trims the CharSequence. + * That functionality is available in isBlank().

+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is empty or null + * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) + */ + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.length() == 0; + } + + /** + *

Checks if a CharSequence is not empty ("") and not null.

+ *

+ *

+     * StringUtils.isNotEmpty(null)      = false
+     * StringUtils.isNotEmpty("")        = false
+     * StringUtils.isNotEmpty(" ")       = true
+     * StringUtils.isNotEmpty("bob")     = true
+     * StringUtils.isNotEmpty("  bob  ") = true
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is not empty and not null + * @since 3.0 Changed signature from isNotEmpty(String) to isNotEmpty(CharSequence) + */ + public static boolean isNotEmpty(final CharSequence cs) { + return !StringUtils.isEmpty(cs); + } + + /** + *

Checks if any one of the CharSequences are empty ("") or null.

+ *

+ *

+     * StringUtils.isAnyEmpty(null)             = true
+     * StringUtils.isAnyEmpty(null, "foo")      = true
+     * StringUtils.isAnyEmpty("", "bar")        = true
+     * StringUtils.isAnyEmpty("bob", "")        = true
+     * StringUtils.isAnyEmpty("  bob  ", null)  = true
+     * StringUtils.isAnyEmpty(" ", "bar")       = false
+     * StringUtils.isAnyEmpty("foo", "bar")     = false
+     * 
+ * + * @param css the CharSequences to check, may be null or empty + * @return {@code true} if any of the CharSequences are empty or null + * @since 3.2 + */ + public static boolean isAnyEmpty(CharSequence... css) { + if (ArrayUtils.isEmpty(css)) { + return true; + } + for (CharSequence cs : css) { + if (isEmpty(cs)) { + return true; + } + } + return false; + } + + /** + *

Checks if none of the CharSequences are empty ("") or null.

+ *

+ *

+     * StringUtils.isNoneEmpty(null)             = false
+     * StringUtils.isNoneEmpty(null, "foo")      = false
+     * StringUtils.isNoneEmpty("", "bar")        = false
+     * StringUtils.isNoneEmpty("bob", "")        = false
+     * StringUtils.isNoneEmpty("  bob  ", null)  = false
+     * StringUtils.isNoneEmpty(" ", "bar")       = true
+     * StringUtils.isNoneEmpty("foo", "bar")     = true
+     * 
+ * + * @param css the CharSequences to check, may be null or empty + * @return {@code true} if none of the CharSequences are empty or null + * @since 3.2 + */ + public static boolean isNoneEmpty(CharSequence... css) { + return !isAnyEmpty(css); + } + + /** + *

Checks if a CharSequence is whitespace, empty ("") or null.

+ *

+ *

+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is null, empty or whitespace + * @since 3.0 Changed signature from isBlank(String) to isBlank(CharSequence) + */ + public static boolean isBlank(final CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

+ *

+ *

+     * StringUtils.isNotBlank(null)      = false
+     * StringUtils.isNotBlank("")        = false
+     * StringUtils.isNotBlank(" ")       = false
+     * StringUtils.isNotBlank("bob")     = true
+     * StringUtils.isNotBlank("  bob  ") = true
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @since 3.0 Changed signature from isNotBlank(String) to isNotBlank(CharSequence) + */ + public static boolean isNotBlank(final CharSequence cs) { + return !StringUtils.isBlank(cs); + } + + /** + *

Checks if any one of the CharSequences are blank ("") or null and not whitespace only..

+ *

+ *

+     * StringUtils.isAnyBlank(null)             = true
+     * StringUtils.isAnyBlank(null, "foo")      = true
+     * StringUtils.isAnyBlank(null, null)       = true
+     * StringUtils.isAnyBlank("", "bar")        = true
+     * StringUtils.isAnyBlank("bob", "")        = true
+     * StringUtils.isAnyBlank("  bob  ", null)  = true
+     * StringUtils.isAnyBlank(" ", "bar")       = true
+     * StringUtils.isAnyBlank("foo", "bar")     = false
+     * 
+ * + * @param css the CharSequences to check, may be null or empty + * @return {@code true} if any of the CharSequences are blank or null or whitespace only + * @since 3.2 + */ + public static boolean isAnyBlank(CharSequence... css) { + if (ArrayUtils.isEmpty(css)) { + return true; + } + for (CharSequence cs : css) { + if (isBlank(cs)) { + return true; + } + } + return false; + } + + /** + *

Checks if none of the CharSequences are blank ("") or null and whitespace only..

+ *

+ *

+     * StringUtils.isNoneBlank(null)             = false
+     * StringUtils.isNoneBlank(null, "foo")      = false
+     * StringUtils.isNoneBlank(null, null)       = false
+     * StringUtils.isNoneBlank("", "bar")        = false
+     * StringUtils.isNoneBlank("bob", "")        = false
+     * StringUtils.isNoneBlank("  bob  ", null)  = false
+     * StringUtils.isNoneBlank(" ", "bar")       = false
+     * StringUtils.isNoneBlank("foo", "bar")     = true
+     * 
+ * + * @param css the CharSequences to check, may be null or empty + * @return {@code true} if none of the CharSequences are blank or null or whitespace only + * @since 3.2 + */ + public static boolean isNoneBlank(CharSequence... css) { + return !isAnyBlank(css); + } + + // Trim + //----------------------------------------------------------------------- + + /** + *

Removes control characters (char <= 32) from both + * ends of this String, handling {@code null} by returning + * {@code null}.

+ *

+ *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #strip(String)}.

+ *

+ *

To trim your choice of characters, use the + * {@link #strip(String, String)} methods.

+ *

+ *

+     * StringUtils.trim(null)          = null
+     * StringUtils.trim("")            = ""
+     * StringUtils.trim("     ")       = ""
+     * StringUtils.trim("abc")         = "abc"
+     * StringUtils.trim("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed string, {@code null} if null String input + */ + public static String trim(final String str) { + return str == null ? null : str.trim(); + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning {@code null} if the String is + * empty ("") after the trim or if it is {@code null}. + *

+ *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToNull(String)}.

+ *

+ *

+     * StringUtils.trimToNull(null)          = null
+     * StringUtils.trimToNull("")            = null
+     * StringUtils.trimToNull("     ")       = null
+     * StringUtils.trimToNull("abc")         = "abc"
+     * StringUtils.trimToNull("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, + * {@code null} if only chars <= 32, empty or null String input + * @since 2.0 + */ + public static String trimToNull(final String str) { + final String ts = trim(str); + return isEmpty(ts) ? null : ts; + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning an empty String ("") if the String + * is empty ("") after the trim or if it is {@code null}. + *

+ *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToEmpty(String)}.

+ *

+ *

+     * StringUtils.trimToEmpty(null)          = ""
+     * StringUtils.trimToEmpty("")            = ""
+     * StringUtils.trimToEmpty("     ")       = ""
+     * StringUtils.trimToEmpty("abc")         = "abc"
+     * StringUtils.trimToEmpty("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, or an empty String if {@code null} input + * @since 2.0 + */ + public static String trimToEmpty(final String str) { + return str == null ? EMPTY : str.trim(); + } + + // Stripping + //----------------------------------------------------------------------- + + /** + *

Strips whitespace from the start and end of a String.

+ *

+ *

This is similar to {@link #trim(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.strip(null)     = null
+     * StringUtils.strip("")       = ""
+     * StringUtils.strip("   ")    = ""
+     * StringUtils.strip("abc")    = "abc"
+     * StringUtils.strip("  abc")  = "abc"
+     * StringUtils.strip("abc  ")  = "abc"
+     * StringUtils.strip(" abc ")  = "abc"
+     * StringUtils.strip(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to remove whitespace from, may be null + * @return the stripped String, {@code null} if null String input + */ + public static String strip(final String str) { + return strip(str, null); + } + + /** + *

Strips whitespace from the start and end of a String returning + * {@code null} if the String is empty ("") after the strip.

+ *

+ *

This is similar to {@link #trimToNull(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.stripToNull(null)     = null
+     * StringUtils.stripToNull("")       = null
+     * StringUtils.stripToNull("   ")    = null
+     * StringUtils.stripToNull("abc")    = "abc"
+     * StringUtils.stripToNull("  abc")  = "abc"
+     * StringUtils.stripToNull("abc  ")  = "abc"
+     * StringUtils.stripToNull(" abc ")  = "abc"
+     * StringUtils.stripToNull(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the stripped String, + * {@code null} if whitespace, empty or null String input + * @since 2.0 + */ + public static String stripToNull(String str) { + if (str == null) { + return null; + } + str = strip(str, null); + return str.isEmpty() ? null : str; + } + + /** + *

Strips whitespace from the start and end of a String returning + * an empty String if {@code null} input.

+ *

+ *

This is similar to {@link #trimToEmpty(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.stripToEmpty(null)     = ""
+     * StringUtils.stripToEmpty("")       = ""
+     * StringUtils.stripToEmpty("   ")    = ""
+     * StringUtils.stripToEmpty("abc")    = "abc"
+     * StringUtils.stripToEmpty("  abc")  = "abc"
+     * StringUtils.stripToEmpty("abc  ")  = "abc"
+     * StringUtils.stripToEmpty(" abc ")  = "abc"
+     * StringUtils.stripToEmpty(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the trimmed String, or an empty String if {@code null} input + * @since 2.0 + */ + public static String stripToEmpty(final String str) { + return str == null ? EMPTY : strip(str, null); + } + + /** + *

Strips any of a set of characters from the start and end of a String. + * This is similar to {@link String#trim()} but allows the characters + * to be stripped to be controlled.

+ *

+ *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ *

+ *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}. + * Alternatively use {@link #strip(String)}.

+ *

+ *

+     * StringUtils.strip(null, *)          = null
+     * StringUtils.strip("", *)            = ""
+     * StringUtils.strip("abc", null)      = "abc"
+     * StringUtils.strip("  abc", null)    = "abc"
+     * StringUtils.strip("abc  ", null)    = "abc"
+     * StringUtils.strip(" abc ", null)    = "abc"
+     * StringUtils.strip("  abcyx", "xyz") = "  abc"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String strip(String str, final String stripChars) { + if (isEmpty(str)) { + return str; + } + str = stripStart(str, stripChars); + return stripEnd(str, stripChars); + } + + /** + *

Strips any of a set of characters from the start of a String.

+ *

+ *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ *

+ *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.stripStart(null, *)          = null
+     * StringUtils.stripStart("", *)            = ""
+     * StringUtils.stripStart("abc", "")        = "abc"
+     * StringUtils.stripStart("abc", null)      = "abc"
+     * StringUtils.stripStart("  abc", null)    = "abc"
+     * StringUtils.stripStart("abc  ", null)    = "abc  "
+     * StringUtils.stripStart(" abc ", null)    = "abc "
+     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String stripStart(final String str, final String stripChars) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + int start = 0; + if (stripChars == null) { + while (start != strLen && Character.isWhitespace(str.charAt(start))) { + start++; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (start != strLen && stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND) { + start++; + } + } + return str.substring(start); + } + + /** + *

Strips any of a set of characters from the end of a String.

+ *

+ *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ *

+ *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the set of characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String stripEnd(final String str, final String stripChars) { + int end; + if (str == null || (end = str.length()) == 0) { + return str; + } + + if (stripChars == null) { + while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) { + end--; + } + } + return str.substring(0, end); + } + + // StripAll + //----------------------------------------------------------------------- + + /** + *

Strips whitespace from the start and end of every String in an array. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

A new array is returned each time, except for length zero. + * A {@code null} array will return {@code null}. + * An empty array will return itself. + * A {@code null} array entry will be ignored.

+ *

+ *

+     * StringUtils.stripAll(null)             = null
+     * StringUtils.stripAll([])               = []
+     * StringUtils.stripAll(["abc", "  abc"]) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null])  = ["abc", null]
+     * 
+ * + * @param strs the array to remove whitespace from, may be null + * @return the stripped Strings, {@code null} if null array input + */ + public static String[] stripAll(final String... strs) { + return stripAll(strs, null); + } + + /** + *

Strips any of a set of characters from the start and end of every + * String in an array.

+ *

Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

A new array is returned each time, except for length zero. + * A {@code null} array will return {@code null}. + * An empty array will return itself. + * A {@code null} array entry will be ignored. + * A {@code null} stripChars will strip whitespace as defined by + * {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.stripAll(null, *)                = null
+     * StringUtils.stripAll([], *)                  = []
+     * StringUtils.stripAll(["abc", "  abc"], null) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null], null)  = ["abc", null]
+     * StringUtils.stripAll(["abc  ", null], "yz")  = ["abc  ", null]
+     * StringUtils.stripAll(["yabcz", null], "yz")  = ["abc", null]
+     * 
+ * + * @param strs the array to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped Strings, {@code null} if null array input + */ + public static String[] stripAll(final String[] strs, final String stripChars) { + int strsLen; + if (strs == null || (strsLen = strs.length) == 0) { + return strs; + } + final String[] newArr = new String[strsLen]; + for (int i = 0; i < strsLen; i++) { + newArr[i] = strip(strs[i], stripChars); + } + return newArr; + } + + /** + *

Removes diacritics (~= accents) from a string. The case will not be altered.

+ *

For instance, 'à' will be replaced by 'a'.

+ *

Note that ligatures will be left as is.

+ *

+ *

+     * StringUtils.stripAccents(null)                = null
+     * StringUtils.stripAccents("")                  = ""
+     * StringUtils.stripAccents("control")           = "control"
+     * StringUtils.stripAccents("éclair")     = "eclair"
+     * 
+ * + * @param input String to be stripped + * @return input text with diacritics removed + * @since 3.0 + */ + // See also Lucene's ASCIIFoldingFilter (Lucene 2.9) that replaces accented characters by their unaccented equivalent (and uncommitted bug fix: https://issues.apache.org/jira/browse/LUCENE-1343?focusedCommentId=12858907&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_12858907). + public static String stripAccents(final String input) { + if (input == null) { + return null; + } + final Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");//$NON-NLS-1$ + final String decomposed = Normalizer.normalize(input, Normalizer.Form.NFD); + // Note that this doesn't correctly remove ligatures... + return pattern.matcher(decomposed).replaceAll("");//$NON-NLS-1$ + } + + // Equals + //----------------------------------------------------------------------- + + /** + *

Compares two CharSequences, returning {@code true} if they represent + * equal sequences of characters.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ *

+ *

+     * StringUtils.equals(null, null)   = true
+     * StringUtils.equals(null, "abc")  = false
+     * StringUtils.equals("abc", null)  = false
+     * StringUtils.equals("abc", "abc") = true
+     * StringUtils.equals("abc", "ABC") = false
+     * 
+ * + * @param cs1 the first CharSequence, may be {@code null} + * @param cs2 the second CharSequence, may be {@code null} + * @return {@code true} if the CharSequences are equal (case-sensitive), or both {@code null} + * @see Object#equals(Object) + * @since 3.0 Changed signature from equals(String, String) to equals(CharSequence, CharSequence) + */ + public static boolean equals(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return true; + } + if (cs1 == null || cs2 == null) { + return false; + } + if (cs1 instanceof String && cs2 instanceof String) { + return cs1.equals(cs2); + } + return CharSequenceUtils.regionMatches(cs1, false, 0, cs2, 0, Math.max(cs1.length(), cs2.length())); + } + + /** + *

Compares two CharSequences, returning {@code true} if they represent + * equal sequences of characters, ignoring case.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered equal. Comparison is case insensitive.

+ *

+ *

+     * StringUtils.equalsIgnoreCase(null, null)   = true
+     * StringUtils.equalsIgnoreCase(null, "abc")  = false
+     * StringUtils.equalsIgnoreCase("abc", null)  = false
+     * StringUtils.equalsIgnoreCase("abc", "abc") = true
+     * StringUtils.equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @param str1 the first CharSequence, may be null + * @param str2 the second CharSequence, may be null + * @return {@code true} if the CharSequence are equal, case insensitive, or + * both {@code null} + * @since 3.0 Changed signature from equalsIgnoreCase(String, String) to equalsIgnoreCase(CharSequence, CharSequence) + */ + public static boolean equalsIgnoreCase(final CharSequence str1, final CharSequence str2) { + if (str1 == null || str2 == null) { + return str1 == str2; + } else if (str1 == str2) { + return true; + } else if (str1.length() != str2.length()) { + return false; + } else { + return CharSequenceUtils.regionMatches(str1, true, 0, str2, 0, str1.length()); + } + } + + // IndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(int, int)} if possible.

+ *

+ *

A {@code null} or empty ("") CharSequence will return {@code INDEX_NOT_FOUND (-1)}.

+ *

+ *

+     * StringUtils.indexOf(null, *)         = -1
+     * StringUtils.indexOf("", *)           = -1
+     * StringUtils.indexOf("aabaabaa", 'a') = 0
+     * StringUtils.indexOf("aabaabaa", 'b') = 2
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return the first index of the search character, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOf(String, int) to indexOf(CharSequence, int) + */ + public static int indexOf(final CharSequence seq, final int searchChar) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchChar, 0); + } + + /** + *

Finds the first index within a CharSequence from a start position, + * handling {@code null}. + * This method uses {@link String#indexOf(int, int)} if possible.

+ *

+ *

A {@code null} or empty ("") CharSequence will return {@code (INDEX_NOT_FOUND) -1}. + * A negative start position is treated as zero. + * A start position greater than the string length returns {@code -1}.

+ *

+ *

+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf("", *, *)            = -1
+     * StringUtils.indexOf("aabaabaa", 'b', 0)  = 2
+     * StringUtils.indexOf("aabaabaa", 'b', 3)  = 5
+     * StringUtils.indexOf("aabaabaa", 'b', 9)  = -1
+     * StringUtils.indexOf("aabaabaa", 'b', -1) = 2
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @param startPos the start position, negative treated as zero + * @return the first index of the search character (always ≥ startPos), + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOf(String, int, int) to indexOf(CharSequence, int, int) + */ + public static int indexOf(final CharSequence seq, final int searchChar, final int startPos) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchChar, startPos); + } + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String, int)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}.

+ *

+ *

+     * StringUtils.indexOf(null, *)          = -1
+     * StringUtils.indexOf(*, null)          = -1
+     * StringUtils.indexOf("", "")           = 0
+     * StringUtils.indexOf("", *)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a")  = 0
+     * StringUtils.indexOf("aabaabaa", "b")  = 2
+     * StringUtils.indexOf("aabaabaa", "ab") = 1
+     * StringUtils.indexOf("aabaabaa", "")   = 0
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOf(String, String) to indexOf(CharSequence, CharSequence) + */ + public static int indexOf(final CharSequence seq, final CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchSeq, 0); + } + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String, int)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ *

+ *

+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf(*, null, *)          = -1
+     * StringUtils.indexOf("", "", 0)           = 0
+     * StringUtils.indexOf("", *, 0)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.indexOf("aabaabaa", "b", 0)  = 2
+     * StringUtils.indexOf("aabaabaa", "ab", 0) = 1
+     * StringUtils.indexOf("aabaabaa", "b", 3)  = 5
+     * StringUtils.indexOf("aabaabaa", "b", 9)  = -1
+     * StringUtils.indexOf("aabaabaa", "b", -1) = 2
+     * StringUtils.indexOf("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOf("abc", "", 9)        = 3
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence (always ≥ startPos), + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOf(String, String, int) to indexOf(CharSequence, CharSequence, int) + */ + public static int indexOf(final CharSequence seq, final CharSequence searchSeq, final int startPos) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchSeq, startPos); + } + + /** + *

Finds the n-th index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}.

+ *

+ *

+     * StringUtils.ordinalIndexOf(null, *, *)          = -1
+     * StringUtils.ordinalIndexOf(*, null, *)          = -1
+     * StringUtils.ordinalIndexOf("", "", *)           = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 1)   = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 2)   = 0
+     * 
+ *

+ *

Note that 'head(CharSequence str, int n)' may be implemented as:

+ *

+ *

+     *   str.substring(0, lastOrdinalIndexOf(str, "\n", n))
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th {@code searchStr} to find + * @return the n-th index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + * @since 3.0 Changed signature from ordinalIndexOf(String, String, int) to ordinalIndexOf(CharSequence, CharSequence, int) + */ + public static int ordinalIndexOf(final CharSequence str, final CharSequence searchStr, final int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, false); + } + + /** + *

Finds the n-th index within a String, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}.

+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th {@code searchStr} to find + * @param lastIndex true if lastOrdinalIndexOf() otherwise false if ordinalIndexOf() + * @return the n-th index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + */ + // Shared code between ordinalIndexOf(String,String,int) and lastOrdinalIndexOf(String,String,int) + private static int ordinalIndexOf(final CharSequence str, final CharSequence searchStr, final int ordinal, final boolean lastIndex) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return lastIndex ? str.length() : 0; + } + int found = 0; + int index = lastIndex ? str.length() : INDEX_NOT_FOUND; + do { + if (lastIndex) { + index = CharSequenceUtils.lastIndexOf(str, searchStr, index - 1); + } else { + index = CharSequenceUtils.indexOf(str, searchStr, index + 1); + } + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + /** + *

Case in-sensitive find of the first index within a CharSequence.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ *

+ *

+     * StringUtils.indexOfIgnoreCase(null, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null)          = -1
+     * StringUtils.indexOfIgnoreCase("", "")           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "a")  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "b")  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "ab") = 1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOfIgnoreCase(String, String) to indexOfIgnoreCase(CharSequence, CharSequence) + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + *

Case in-sensitive find of the first index within a CharSequence + * from the specified position.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ *

+ *

+     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = 3
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence (always ≥ startPos), + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from indexOfIgnoreCase(String, String, int) to indexOfIgnoreCase(CharSequence, CharSequence, int) + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos < 0) { + startPos = 0; + } + final int endLimit = str.length() - searchStr.length() + 1; + if (startPos > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + for (int i = startPos; i < endLimit; i++) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // LastIndexOf + //----------------------------------------------------------------------- + + /** + *

Finds the last index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(int)} if possible.

+ *

+ *

A {@code null} or empty ("") CharSequence will return {@code -1}.

+ *

+ *

+     * StringUtils.lastIndexOf(null, *)         = -1
+     * StringUtils.lastIndexOf("", *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a') = 7
+     * StringUtils.lastIndexOf("aabaabaa", 'b') = 5
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return the last index of the search character, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from lastIndexOf(String, int) to lastIndexOf(CharSequence, int) + */ + public static int lastIndexOf(final CharSequence seq, final int searchChar) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchChar, seq.length()); + } + + /** + *

Finds the last index within a CharSequence from a start position, + * handling {@code null}. + * This method uses {@link String#lastIndexOf(int, int)} if possible.

+ *

+ *

A {@code null} or empty ("") CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * A start position greater than the string length searches the whole string. + * The search starts at the startPos and works backwards; matches starting after the start + * position are ignored. + *

+ *

+ *

+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf("", *,  *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 4)  = 2
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 0)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a', 0)  = 0
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @param startPos the start position + * @return the last index of the search character (always ≤ startPos), + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from lastIndexOf(String, int, int) to lastIndexOf(CharSequence, int, int) + */ + public static int lastIndexOf(final CharSequence seq, final int searchChar, final int startPos) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchChar, startPos); + } + + /** + *

Finds the last index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(String)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}.

+ *

+ *

+     * StringUtils.lastIndexOf(null, *)          = -1
+     * StringUtils.lastIndexOf(*, null)          = -1
+     * StringUtils.lastIndexOf("", "")           = 0
+     * StringUtils.lastIndexOf("aabaabaa", "a")  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b")  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab") = 4
+     * StringUtils.lastIndexOf("aabaabaa", "")   = 8
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return the last index of the search String, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from lastIndexOf(String, String) to lastIndexOf(CharSequence, CharSequence) + */ + public static int lastIndexOf(final CharSequence seq, final CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchSeq, seq.length()); + } + + /** + *

Finds the n-th last index within a String, handling {@code null}. + * This method uses {@link String#lastIndexOf(String)}.

+ *

+ *

A {@code null} String will return {@code -1}.

+ *

+ *

+     * StringUtils.lastOrdinalIndexOf(null, *, *)          = -1
+     * StringUtils.lastOrdinalIndexOf(*, null, *)          = -1
+     * StringUtils.lastOrdinalIndexOf("", "", *)           = 0
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 1)  = 7
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 2)  = 6
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 1)  = 5
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 2)  = 2
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 1) = 4
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 2) = 1
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 1)   = 8
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 2)   = 8
+     * 
+ *

+ *

Note that 'tail(CharSequence str, int n)' may be implemented as:

+ *

+ *

+     *   str.substring(lastOrdinalIndexOf(str, "\n", n) + 1)
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th last {@code searchStr} to find + * @return the n-th last index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + * @since 3.0 Changed signature from lastOrdinalIndexOf(String, String, int) to lastOrdinalIndexOf(CharSequence, CharSequence, int) + */ + public static int lastOrdinalIndexOf(final CharSequence str, final CharSequence searchStr, final int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, true); + } + + /** + *

Finds the last index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(String, int)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string. + * The search starts at the startPos and works backwards; matches starting after the start + * position are ignored. + *

+ *

+ *

+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf(*, null, *)          = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 8)  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b", 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab", 8) = 4
+     * StringUtils.lastIndexOf("aabaabaa", "b", 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "b", -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.lastIndexOf("aabaabaa", "b", 0)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", "b", 1)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", "b", 2)  = 2
+     * StringUtils.lastIndexOf("aabaabaa", "ba", 2)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", "ba", 2)  = 2
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the last index of the search CharSequence (always ≤ startPos), + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from lastIndexOf(String, String, int) to lastIndexOf(CharSequence, CharSequence, int) + */ + public static int lastIndexOf(final CharSequence seq, final CharSequence searchSeq, final int startPos) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchSeq, startPos); + } + + /** + *

Case in-sensitive find of the last index within a CharSequence.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ *

+ *

+     * StringUtils.lastIndexOfIgnoreCase(null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A")  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B")  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB") = 4
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String) to lastIndexOfIgnoreCase(CharSequence, CharSequence) + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + *

Case in-sensitive find of the last index within a CharSequence + * from the specified position.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string. + * The search starts at the startPos and works backwards; matches starting after the start + * position are ignored. + *

+ *

+ *

+     * StringUtils.lastIndexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8)  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", -1) = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 0)  = -1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param startPos the start position + * @return the last index of the search CharSequence (always ≤ startPos), + * -1 if no match or {@code null} input + * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String, int) to lastIndexOfIgnoreCase(CharSequence, CharSequence, int) + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos > str.length() - searchStr.length()) { + startPos = str.length() - searchStr.length(); + } + if (startPos < 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + + for (int i = startPos; i >= 0; i--) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // Contains + //----------------------------------------------------------------------- + + /** + *

Checks if CharSequence contains a search character, handling {@code null}. + * This method uses {@link String#indexOf(int)} if possible.

+ *

+ *

A {@code null} or empty ("") CharSequence will return {@code false}.

+ *

+ *

+     * StringUtils.contains(null, *)    = false
+     * StringUtils.contains("", *)      = false
+     * StringUtils.contains("abc", 'a') = true
+     * StringUtils.contains("abc", 'z') = false
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return true if the CharSequence contains the search character, + * false if not or {@code null} string input + * @since 3.0 Changed signature from contains(String, int) to contains(CharSequence, int) + */ + public static boolean contains(final CharSequence seq, final int searchChar) { + if (isEmpty(seq)) { + return false; + } + return CharSequenceUtils.indexOf(seq, searchChar, 0) >= 0; + } + + /** + *

Checks if CharSequence contains a search CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ *

+ *

A {@code null} CharSequence will return {@code false}.

+ *

+ *

+     * StringUtils.contains(null, *)     = false
+     * StringUtils.contains(*, null)     = false
+     * StringUtils.contains("", "")      = true
+     * StringUtils.contains("abc", "")   = true
+     * StringUtils.contains("abc", "a")  = true
+     * StringUtils.contains("abc", "z")  = false
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence, + * false if not or {@code null} string input + * @since 3.0 Changed signature from contains(String, String) to contains(CharSequence, CharSequence) + */ + public static boolean contains(final CharSequence seq, final CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return false; + } + return CharSequenceUtils.indexOf(seq, searchSeq, 0) >= 0; + } + + /** + *

Checks if CharSequence contains a search CharSequence irrespective of case, + * handling {@code null}. Case-insensitivity is defined as by + * {@link String#equalsIgnoreCase(String)}. + *

+ *

A {@code null} CharSequence will return {@code false}.

+ *

+ *

+     * StringUtils.contains(null, *) = false
+     * StringUtils.contains(*, null) = false
+     * StringUtils.contains("", "") = true
+     * StringUtils.contains("abc", "") = true
+     * StringUtils.contains("abc", "a") = true
+     * StringUtils.contains("abc", "z") = false
+     * StringUtils.contains("abc", "A") = true
+     * StringUtils.contains("abc", "Z") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence irrespective of + * case or false if not or {@code null} string input + * @since 3.0 Changed signature from containsIgnoreCase(String, String) to containsIgnoreCase(CharSequence, CharSequence) + */ + public static boolean containsIgnoreCase(final CharSequence str, final CharSequence searchStr) { + if (str == null || searchStr == null) { + return false; + } + final int len = searchStr.length(); + final int max = str.length() - len; + for (int i = 0; i <= max; i++) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, len)) { + return true; + } + } + return false; + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * + * @param seq the CharSequence to check (may be {@code null}) + * @return {@code true} if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see Character#isWhitespace + * @since 3.0 + */ + // From org.springframework.util.StringUtils, under Apache License 2.0 + public static boolean containsWhitespace(final CharSequence seq) { + if (isEmpty(seq)) { + return false; + } + final int strLen = seq.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(seq.charAt(i))) { + return true; + } + } + return false; + } + + // IndexOfAny chars + //----------------------------------------------------------------------- + + /** + *

Search a CharSequence to find the first index of any + * character in the given set of characters.

+ *

+ *

A {@code null} String will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}.

+ *

+ *

+     * StringUtils.indexOfAny(null, *)                = -1
+     * StringUtils.indexOfAny("", *)                  = -1
+     * StringUtils.indexOfAny(*, null)                = -1
+     * StringUtils.indexOfAny(*, [])                  = -1
+     * StringUtils.indexOfAny("zzabyycdxx",['z','a']) = 0
+     * StringUtils.indexOfAny("zzabyycdxx",['b','y']) = 3
+     * StringUtils.indexOfAny("aba", ['z'])           = -1
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 3.0 Changed signature from indexOfAny(String, char[]) to indexOfAny(CharSequence, char...) + */ + public static int indexOfAny(final CharSequence cs, final char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + final int csLen = cs.length(); + final int csLast = csLen - 1; + final int searchLen = searchChars.length; + final int searchLast = searchLen - 1; + for (int i = 0; i < csLen; i++) { + final char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (i < csLast && j < searchLast && Character.isHighSurrogate(ch)) { + // ch is a supplementary character + if (searchChars[j + 1] == cs.charAt(i + 1)) { + return i; + } + } else { + return i; + } + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Search a CharSequence to find the first index of any + * character in the given set of characters.

+ *

+ *

A {@code null} String will return {@code -1}. + * A {@code null} search string will return {@code -1}.

+ *

+ *

+     * StringUtils.indexOfAny(null, *)            = -1
+     * StringUtils.indexOfAny("", *)              = -1
+     * StringUtils.indexOfAny(*, null)            = -1
+     * StringUtils.indexOfAny(*, "")              = -1
+     * StringUtils.indexOfAny("zzabyycdxx", "za") = 0
+     * StringUtils.indexOfAny("zzabyycdxx", "by") = 3
+     * StringUtils.indexOfAny("aba","z")          = -1
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 3.0 Changed signature from indexOfAny(String, String) to indexOfAny(CharSequence, String) + */ + public static int indexOfAny(final CharSequence cs, final String searchChars) { + if (isEmpty(cs) || isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + return indexOfAny(cs, searchChars.toCharArray()); + } + + // ContainsAny + //----------------------------------------------------------------------- + + /** + *

Checks if the CharSequence contains any character in the given + * set of characters.

+ *

+ *

A {@code null} CharSequence will return {@code false}. + * A {@code null} or zero length search array will return {@code false}.

+ *

+ *

+     * StringUtils.containsAny(null, *)                = false
+     * StringUtils.containsAny("", *)                  = false
+     * StringUtils.containsAny(*, null)                = false
+     * StringUtils.containsAny(*, [])                  = false
+     * StringUtils.containsAny("zzabyycdxx",['z','a']) = true
+     * StringUtils.containsAny("zzabyycdxx",['b','y']) = true
+     * StringUtils.containsAny("aba", ['z'])           = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the {@code true} if any of the chars are found, + * {@code false} if no match or null input + * @since 3.0 Changed signature from containsAny(String, char[]) to containsAny(CharSequence, char...) + */ + public static boolean containsAny(final CharSequence cs, final char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return false; + } + final int csLength = cs.length(); + final int searchLength = searchChars.length; + final int csLast = csLength - 1; + final int searchLast = searchLength - 1; + for (int i = 0; i < csLength; i++) { + final char ch = cs.charAt(i); + for (int j = 0; j < searchLength; j++) { + if (searchChars[j] == ch) { + if (Character.isHighSurrogate(ch)) { + if (j == searchLast) { + // missing low surrogate, fine, like String.indexOf(String) + return true; + } + if (i < csLast && searchChars[j + 1] == cs.charAt(i + 1)) { + return true; + } + } else { + // ch is in the Basic Multilingual Plane + return true; + } + } + } + } + return false; + } + + /** + *

+ * Checks if the CharSequence contains any character in the given set of characters. + *

+ *

+ *

+ * A {@code null} CharSequence will return {@code false}. A {@code null} search CharSequence will return + * {@code false}. + *

+ *

+ *

+     * StringUtils.containsAny(null, *)            = false
+     * StringUtils.containsAny("", *)              = false
+     * StringUtils.containsAny(*, null)            = false
+     * StringUtils.containsAny(*, "")              = false
+     * StringUtils.containsAny("zzabyycdxx", "za") = true
+     * StringUtils.containsAny("zzabyycdxx", "by") = true
+     * StringUtils.containsAny("aba","z")          = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the {@code true} if any of the chars are found, {@code false} if no match or null input + * @since 3.0 Changed signature from containsAny(String, String) to containsAny(CharSequence, CharSequence) + */ + public static boolean containsAny(final CharSequence cs, final CharSequence searchChars) { + if (searchChars == null) { + return false; + } + return containsAny(cs, CharSequenceUtils.toCharArray(searchChars)); + } + + // IndexOfAnyBut chars + //----------------------------------------------------------------------- + + /** + *

Searches a CharSequence to find the first index of any + * character not in the given set of characters.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}.

+ *

+ *

+     * StringUtils.indexOfAnyBut(null, *)                              = -1
+     * StringUtils.indexOfAnyBut("", *)                                = -1
+     * StringUtils.indexOfAnyBut(*, null)                              = -1
+     * StringUtils.indexOfAnyBut(*, [])                                = -1
+     * StringUtils.indexOfAnyBut("zzabyycdxx", new char[] {'z', 'a'} ) = 3
+     * StringUtils.indexOfAnyBut("aba", new char[] {'z'} )             = 0
+     * StringUtils.indexOfAnyBut("aba", new char[] {'a', 'b'} )        = -1
+     *
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 3.0 Changed signature from indexOfAnyBut(String, char[]) to indexOfAnyBut(CharSequence, char...) + */ + public static int indexOfAnyBut(final CharSequence cs, final char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + final int csLen = cs.length(); + final int csLast = csLen - 1; + final int searchLen = searchChars.length; + final int searchLast = searchLen - 1; + outer: + for (int i = 0; i < csLen; i++) { + final char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (i < csLast && j < searchLast && Character.isHighSurrogate(ch)) { + if (searchChars[j + 1] == cs.charAt(i + 1)) { + continue outer; + } + } else { + continue outer; + } + } + } + return i; + } + return INDEX_NOT_FOUND; + } + + /** + *

Search a CharSequence to find the first index of any + * character not in the given set of characters.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or empty search string will return {@code -1}.

+ *

+ *

+     * StringUtils.indexOfAnyBut(null, *)            = -1
+     * StringUtils.indexOfAnyBut("", *)              = -1
+     * StringUtils.indexOfAnyBut(*, null)            = -1
+     * StringUtils.indexOfAnyBut(*, "")              = -1
+     * StringUtils.indexOfAnyBut("zzabyycdxx", "za") = 3
+     * StringUtils.indexOfAnyBut("zzabyycdxx", "")   = -1
+     * StringUtils.indexOfAnyBut("aba","ab")         = -1
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 3.0 Changed signature from indexOfAnyBut(String, String) to indexOfAnyBut(CharSequence, CharSequence) + */ + public static int indexOfAnyBut(final CharSequence seq, final CharSequence searchChars) { + if (isEmpty(seq) || isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + final int strLen = seq.length(); + for (int i = 0; i < strLen; i++) { + final char ch = seq.charAt(i); + final boolean chFound = CharSequenceUtils.indexOf(searchChars, ch, 0) >= 0; + if (i + 1 < strLen && Character.isHighSurrogate(ch)) { + final char ch2 = seq.charAt(i + 1); + if (chFound && CharSequenceUtils.indexOf(searchChars, ch2, 0) < 0) { + return i; + } + } else { + if (!chFound) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + // ContainsOnly + //----------------------------------------------------------------------- + + /** + *

Checks if the CharSequence contains only certain characters.

+ *

+ *

A {@code null} CharSequence will return {@code false}. + * A {@code null} valid character array will return {@code false}. + * An empty CharSequence (length()=0) always returns {@code true}.

+ *

+ *

+     * StringUtils.containsOnly(null, *)       = false
+     * StringUtils.containsOnly(*, null)       = false
+     * StringUtils.containsOnly("", *)         = true
+     * StringUtils.containsOnly("ab", '')      = false
+     * StringUtils.containsOnly("abab", 'abc') = true
+     * StringUtils.containsOnly("ab1", 'abc')  = false
+     * StringUtils.containsOnly("abz", 'abc')  = false
+     * 
+ * + * @param cs the String to check, may be null + * @param valid an array of valid chars, may be null + * @return true if it only contains valid chars and is non-null + * @since 3.0 Changed signature from containsOnly(String, char[]) to containsOnly(CharSequence, char...) + */ + public static boolean containsOnly(final CharSequence cs, final char... valid) { + // All these pre-checks are to maintain API with an older version + if (valid == null || cs == null) { + return false; + } + if (cs.length() == 0) { + return true; + } + if (valid.length == 0) { + return false; + } + return indexOfAnyBut(cs, valid) == INDEX_NOT_FOUND; + } + + /** + *

Checks if the CharSequence contains only certain characters.

+ *

+ *

A {@code null} CharSequence will return {@code false}. + * A {@code null} valid character String will return {@code false}. + * An empty String (length()=0) always returns {@code true}.

+ *

+ *

+     * StringUtils.containsOnly(null, *)       = false
+     * StringUtils.containsOnly(*, null)       = false
+     * StringUtils.containsOnly("", *)         = true
+     * StringUtils.containsOnly("ab", "")      = false
+     * StringUtils.containsOnly("abab", "abc") = true
+     * StringUtils.containsOnly("ab1", "abc")  = false
+     * StringUtils.containsOnly("abz", "abc")  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param validChars a String of valid chars, may be null + * @return true if it only contains valid chars and is non-null + * @since 3.0 Changed signature from containsOnly(String, String) to containsOnly(CharSequence, String) + */ + public static boolean containsOnly(final CharSequence cs, final String validChars) { + if (cs == null || validChars == null) { + return false; + } + return containsOnly(cs, validChars.toCharArray()); + } + + // ContainsNone + //----------------------------------------------------------------------- + + /** + *

Checks that the CharSequence does not contain certain characters.

+ *

+ *

A {@code null} CharSequence will return {@code true}. + * A {@code null} invalid character array will return {@code true}. + * An empty CharSequence (length()=0) always returns true.

+ *

+ *

+     * StringUtils.containsNone(null, *)       = true
+     * StringUtils.containsNone(*, null)       = true
+     * StringUtils.containsNone("", *)         = true
+     * StringUtils.containsNone("ab", '')      = true
+     * StringUtils.containsNone("abab", 'xyz') = true
+     * StringUtils.containsNone("ab1", 'xyz')  = true
+     * StringUtils.containsNone("abz", 'xyz')  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars an array of invalid chars, may be null + * @return true if it contains none of the invalid chars, or is null + * @since 3.0 Changed signature from containsNone(String, char[]) to containsNone(CharSequence, char...) + */ + public static boolean containsNone(final CharSequence cs, final char... searchChars) { + if (cs == null || searchChars == null) { + return true; + } + final int csLen = cs.length(); + final int csLast = csLen - 1; + final int searchLen = searchChars.length; + final int searchLast = searchLen - 1; + for (int i = 0; i < csLen; i++) { + final char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (Character.isHighSurrogate(ch)) { + if (j == searchLast) { + // missing low surrogate, fine, like String.indexOf(String) + return false; + } + if (i < csLast && searchChars[j + 1] == cs.charAt(i + 1)) { + return false; + } + } else { + // ch is in the Basic Multilingual Plane + return false; + } + } + } + } + return true; + } + + /** + *

Checks that the CharSequence does not contain certain characters.

+ *

+ *

A {@code null} CharSequence will return {@code true}. + * A {@code null} invalid character array will return {@code true}. + * An empty String ("") always returns true.

+ *

+ *

+     * StringUtils.containsNone(null, *)       = true
+     * StringUtils.containsNone(*, null)       = true
+     * StringUtils.containsNone("", *)         = true
+     * StringUtils.containsNone("ab", "")      = true
+     * StringUtils.containsNone("abab", "xyz") = true
+     * StringUtils.containsNone("ab1", "xyz")  = true
+     * StringUtils.containsNone("abz", "xyz")  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param invalidChars a String of invalid chars, may be null + * @return true if it contains none of the invalid chars, or is null + * @since 3.0 Changed signature from containsNone(String, String) to containsNone(CharSequence, String) + */ + public static boolean containsNone(final CharSequence cs, final String invalidChars) { + if (cs == null || invalidChars == null) { + return true; + } + return containsNone(cs, invalidChars.toCharArray()); + } + + // IndexOfAny strings + //----------------------------------------------------------------------- + + /** + *

Find the first index of any of a set of potential substrings.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}. + * A {@code null} search array entry will be ignored, but a search + * array containing "" will return {@code 0} if {@code str} is not + * null. This method uses {@link String#indexOf(String)} if possible.

+ *

+ *

+     * StringUtils.indexOfAny(null, *)                     = -1
+     * StringUtils.indexOfAny(*, null)                     = -1
+     * StringUtils.indexOfAny(*, [])                       = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["ab","cd"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["cd","ab"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["mn","op"])   = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["zab","aby"]) = 1
+     * StringUtils.indexOfAny("zzabyycdxx", [""])          = 0
+     * StringUtils.indexOfAny("", [""])                    = 0
+     * StringUtils.indexOfAny("", ["a"])                   = -1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStrs the CharSequences to search for, may be null + * @return the first index of any of the searchStrs in str, -1 if no match + * @since 3.0 Changed signature from indexOfAny(String, String[]) to indexOfAny(CharSequence, CharSequence...) + */ + public static int indexOfAny(final CharSequence str, final CharSequence... searchStrs) { + if (str == null || searchStrs == null) { + return INDEX_NOT_FOUND; + } + final int sz = searchStrs.length; + + // String's can't have a MAX_VALUEth index. + int ret = Integer.MAX_VALUE; + + int tmp = 0; + for (int i = 0; i < sz; i++) { + final CharSequence search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = CharSequenceUtils.indexOf(str, search, 0); + if (tmp == INDEX_NOT_FOUND) { + continue; + } + + if (tmp < ret) { + ret = tmp; + } + } + + return ret == Integer.MAX_VALUE ? INDEX_NOT_FOUND : ret; + } + + /** + *

Find the latest index of any of a set of potential substrings.

+ *

+ *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} search array will return {@code -1}. + * A {@code null} or zero length search array entry will be ignored, + * but a search array containing "" will return the length of {@code str} + * if {@code str} is not null. This method uses {@link String#indexOf(String)} if possible

+ *

+ *

+     * StringUtils.lastIndexOfAny(null, *)                   = -1
+     * StringUtils.lastIndexOfAny(*, null)                   = -1
+     * StringUtils.lastIndexOfAny(*, [])                     = -1
+     * StringUtils.lastIndexOfAny(*, [null])                 = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["ab","cd"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["cd","ab"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn",""])   = 10
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStrs the CharSequences to search for, may be null + * @return the last index of any of the CharSequences, -1 if no match + * @since 3.0 Changed signature from lastIndexOfAny(String, String[]) to lastIndexOfAny(CharSequence, CharSequence) + */ + public static int lastIndexOfAny(final CharSequence str, final CharSequence... searchStrs) { + if (str == null || searchStrs == null) { + return INDEX_NOT_FOUND; + } + final int sz = searchStrs.length; + int ret = INDEX_NOT_FOUND; + int tmp = 0; + for (int i = 0; i < sz; i++) { + final CharSequence search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = CharSequenceUtils.lastIndexOf(str, search, str.length()); + if (tmp > ret) { + ret = tmp; + } + } + return ret; + } + + // Substring + //----------------------------------------------------------------------- + + /** + *

Gets a substring from the specified String avoiding exceptions.

+ *

+ *

A negative start position can be used to start {@code n} + * characters from the end of the String.

+ *

+ *

A {@code null} String will return {@code null}. + * An empty ("") String will return "".

+ *

+ *

+     * StringUtils.substring(null, *)   = null
+     * StringUtils.substring("", *)     = ""
+     * StringUtils.substring("abc", 0)  = "abc"
+     * StringUtils.substring("abc", 2)  = "c"
+     * StringUtils.substring("abc", 4)  = ""
+     * StringUtils.substring("abc", -2) = "bc"
+     * StringUtils.substring("abc", -4) = "abc"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @return substring from start position, {@code null} if null String input + */ + public static String substring(final String str, int start) { + if (str == null) { + return null; + } + + // handle negatives, which means last n characters + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return EMPTY; + } + + return str.substring(start); + } + + /** + *

Gets a substring from the specified String avoiding exceptions.

+ *

+ *

A negative start position can be used to start/end {@code n} + * characters from the end of the String.

+ *

+ *

The returned substring starts with the character in the {@code start} + * position and ends before the {@code end} position. All position counting is + * zero-based -- i.e., to start at the beginning of the string use + * {@code start = 0}. Negative start and end positions can be used to + * specify offsets relative to the end of the String.

+ *

+ *

If {@code start} is not strictly to the left of {@code end}, "" + * is returned.

+ *

+ *

+     * StringUtils.substring(null, *, *)    = null
+     * StringUtils.substring("", * ,  *)    = "";
+     * StringUtils.substring("abc", 0, 2)   = "ab"
+     * StringUtils.substring("abc", 2, 0)   = ""
+     * StringUtils.substring("abc", 2, 4)   = "c"
+     * StringUtils.substring("abc", 4, 6)   = ""
+     * StringUtils.substring("abc", 2, 2)   = ""
+     * StringUtils.substring("abc", -2, -1) = "b"
+     * StringUtils.substring("abc", -4, 2)  = "ab"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @param end the position to end at (exclusive), negative means + * count back from the end of the String by this many characters + * @return substring from start position to end position, + * {@code null} if null String input + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return null; + } + + // handle negatives + if (end < 0) { + end = str.length() + end; // remember end is negative + } + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + // check length next + if (end > str.length()) { + end = str.length(); + } + + // if start is greater than end, return "" + if (start > end) { + return EMPTY; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + // Left/Right/Mid + //----------------------------------------------------------------------- + + /** + *

Gets the leftmost {@code len} characters of a String.

+ *

+ *

If {@code len} characters are not available, or the + * String is {@code null}, the String will be returned without + * an exception. An empty String is returned if len is negative.

+ *

+ *

+     * StringUtils.left(null, *)    = null
+     * StringUtils.left(*, -ve)     = ""
+     * StringUtils.left("", *)      = ""
+     * StringUtils.left("abc", 0)   = ""
+     * StringUtils.left("abc", 2)   = "ab"
+     * StringUtils.left("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the leftmost characters from, may be null + * @param len the length of the required String + * @return the leftmost characters, {@code null} if null String input + */ + public static String left(final String str, final int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(0, len); + } + + /** + *

Gets the rightmost {@code len} characters of a String.

+ *

+ *

If {@code len} characters are not available, or the String + * is {@code null}, the String will be returned without an + * an exception. An empty String is returned if len is negative.

+ *

+ *

+     * StringUtils.right(null, *)    = null
+     * StringUtils.right(*, -ve)     = ""
+     * StringUtils.right("", *)      = ""
+     * StringUtils.right("abc", 0)   = ""
+     * StringUtils.right("abc", 2)   = "bc"
+     * StringUtils.right("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the rightmost characters from, may be null + * @param len the length of the required String + * @return the rightmost characters, {@code null} if null String input + */ + public static String right(final String str, final int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(str.length() - len); + } + + /** + *

Gets {@code len} characters from the middle of a String.

+ *

+ *

If {@code len} characters are not available, the remainder + * of the String will be returned without an exception. If the + * String is {@code null}, {@code null} will be returned. + * An empty String is returned if len is negative or exceeds the + * length of {@code str}.

+ *

+ *

+     * StringUtils.mid(null, *, *)    = null
+     * StringUtils.mid(*, *, -ve)     = ""
+     * StringUtils.mid("", 0, *)      = ""
+     * StringUtils.mid("abc", 0, 2)   = "ab"
+     * StringUtils.mid("abc", 0, 4)   = "abc"
+     * StringUtils.mid("abc", 2, 4)   = "c"
+     * StringUtils.mid("abc", 4, 2)   = ""
+     * StringUtils.mid("abc", -2, 2)  = "ab"
+     * 
+ * + * @param str the String to get the characters from, may be null + * @param pos the position to start from, negative treated as zero + * @param len the length of the required String + * @return the middle characters, {@code null} if null String input + */ + public static String mid(final String str, int pos, final int len) { + if (str == null) { + return null; + } + if (len < 0 || pos > str.length()) { + return EMPTY; + } + if (pos < 0) { + pos = 0; + } + if (str.length() <= pos + len) { + return str.substring(pos); + } + return str.substring(pos, pos + len); + } + + // SubStringAfter/SubStringBefore + //----------------------------------------------------------------------- + + /** + *

Gets the substring before the first occurrence of a separator. + * The separator is not returned.

+ *

+ *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the input string.

+ *

+ *

If nothing is found, the string input is returned.

+ *

+ *

+     * StringUtils.substringBefore(null, *)      = null
+     * StringUtils.substringBefore("", *)        = ""
+     * StringUtils.substringBefore("abc", "a")   = ""
+     * StringUtils.substringBefore("abcba", "b") = "a"
+     * StringUtils.substringBefore("abc", "c")   = "ab"
+     * StringUtils.substringBefore("abc", "d")   = "abc"
+     * StringUtils.substringBefore("abc", "")    = ""
+     * StringUtils.substringBefore("abc", null)  = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringBefore(final String str, final String separator) { + if (isEmpty(str) || separator == null) { + return str; + } + if (separator.isEmpty()) { + return EMPTY; + } + final int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the first occurrence of a separator. + * The separator is not returned.

+ *

+ *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the empty string if the + * input string is not {@code null}.

+ *

+ *

If nothing is found, the empty string is returned.

+ *

+ *

+     * StringUtils.substringAfter(null, *)      = null
+     * StringUtils.substringAfter("", *)        = ""
+     * StringUtils.substringAfter(*, null)      = ""
+     * StringUtils.substringAfter("abc", "a")   = "bc"
+     * StringUtils.substringAfter("abcba", "b") = "cba"
+     * StringUtils.substringAfter("abc", "c")   = ""
+     * StringUtils.substringAfter("abc", "d")   = ""
+     * StringUtils.substringAfter("abc", "")    = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringAfter(final String str, final String separator) { + if (isEmpty(str)) { + return str; + } + if (separator == null) { + return EMPTY; + } + final int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + *

Gets the substring before the last occurrence of a separator. + * The separator is not returned.

+ *

+ *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * An empty or {@code null} separator will return the input string.

+ *

+ *

If nothing is found, the string input is returned.

+ *

+ *

+     * StringUtils.substringBeforeLast(null, *)      = null
+     * StringUtils.substringBeforeLast("", *)        = ""
+     * StringUtils.substringBeforeLast("abcba", "b") = "abc"
+     * StringUtils.substringBeforeLast("abc", "c")   = "ab"
+     * StringUtils.substringBeforeLast("a", "a")     = ""
+     * StringUtils.substringBeforeLast("a", "z")     = "a"
+     * StringUtils.substringBeforeLast("a", null)    = "a"
+     * StringUtils.substringBeforeLast("a", "")      = "a"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the last occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringBeforeLast(final String str, final String separator) { + if (isEmpty(str) || isEmpty(separator)) { + return str; + } + final int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the last occurrence of a separator. + * The separator is not returned.

+ *

+ *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * An empty or {@code null} separator will return the empty string if + * the input string is not {@code null}.

+ *

+ *

If nothing is found, the empty string is returned.

+ *

+ *

+     * StringUtils.substringAfterLast(null, *)      = null
+     * StringUtils.substringAfterLast("", *)        = ""
+     * StringUtils.substringAfterLast(*, "")        = ""
+     * StringUtils.substringAfterLast(*, null)      = ""
+     * StringUtils.substringAfterLast("abc", "a")   = "bc"
+     * StringUtils.substringAfterLast("abcba", "b") = "a"
+     * StringUtils.substringAfterLast("abc", "c")   = ""
+     * StringUtils.substringAfterLast("a", "a")     = ""
+     * StringUtils.substringAfterLast("a", "z")     = ""
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the last occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringAfterLast(final String str, final String separator) { + if (isEmpty(str)) { + return str; + } + if (isEmpty(separator)) { + return EMPTY; + } + final int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND || pos == str.length() - separator.length()) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + // Substring between + //----------------------------------------------------------------------- + + /** + *

Gets the String that is nested in between two instances of the + * same String.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} tag returns {@code null}.

+ *

+ *

+     * StringUtils.substringBetween(null, *)            = null
+     * StringUtils.substringBetween("", "")             = ""
+     * StringUtils.substringBetween("", "tag")          = null
+     * StringUtils.substringBetween("tagabctag", null)  = null
+     * StringUtils.substringBetween("tagabctag", "")    = ""
+     * StringUtils.substringBetween("tagabctag", "tag") = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param tag the String before and after the substring, may be null + * @return the substring, {@code null} if no match + * @since 2.0 + */ + public static String substringBetween(final String str, final String tag) { + return substringBetween(str, tag, tag); + } + + /** + *

Gets the String that is nested in between two Strings. + * Only the first match is returned.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} open/close returns {@code null} (no match). + * An empty ("") open and close returns an empty string.

+ *

+ *

+     * StringUtils.substringBetween("wx[b]yz", "[", "]") = "b"
+     * StringUtils.substringBetween(null, *, *)          = null
+     * StringUtils.substringBetween(*, null, *)          = null
+     * StringUtils.substringBetween(*, *, null)          = null
+     * StringUtils.substringBetween("", "", "")          = ""
+     * StringUtils.substringBetween("", "", "]")         = null
+     * StringUtils.substringBetween("", "[", "]")        = null
+     * StringUtils.substringBetween("yabcz", "", "")     = ""
+     * StringUtils.substringBetween("yabcz", "y", "z")   = "abc"
+     * StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param open the String before the substring, may be null + * @param close the String after the substring, may be null + * @return the substring, {@code null} if no match + * @since 2.0 + */ + public static String substringBetween(final String str, final String open, final String close) { + if (str == null || open == null || close == null) { + return null; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) { + final int end = str.indexOf(close, start + open.length()); + if (end != INDEX_NOT_FOUND) { + return str.substring(start + open.length(), end); + } + } + return null; + } + + /** + *

Searches a String for substrings delimited by a start and end tag, + * returning all matching substrings in an array.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} open/close returns {@code null} (no match). + * An empty ("") open/close returns {@code null} (no match).

+ *

+ *

+     * StringUtils.substringsBetween("[a][b][c]", "[", "]") = ["a","b","c"]
+     * StringUtils.substringsBetween(null, *, *)            = null
+     * StringUtils.substringsBetween(*, null, *)            = null
+     * StringUtils.substringsBetween(*, *, null)            = null
+     * StringUtils.substringsBetween("", "[", "]")          = []
+     * 
+ * + * @param str the String containing the substrings, null returns null, empty returns empty + * @param open the String identifying the start of the substring, empty returns null + * @param close the String identifying the end of the substring, empty returns null + * @return a String Array of substrings, or {@code null} if no match + * @since 2.3 + */ + public static String[] substringsBetween(final String str, final String open, final String close) { + if (str == null || isEmpty(open) || isEmpty(close)) { + return null; + } + final int strLen = str.length(); + if (strLen == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + final int closeLen = close.length(); + final int openLen = open.length(); + final List list = new ArrayList(); + int pos = 0; + while (pos < strLen - closeLen) { + int start = str.indexOf(open, pos); + if (start < 0) { + break; + } + start += openLen; + final int end = str.indexOf(close, start); + if (end < 0) { + break; + } + list.add(str.substring(start, end)); + pos = end + closeLen; + } + if (list.isEmpty()) { + return null; + } + return list.toArray(new String[list.size()]); + } + + // Nested extraction + //----------------------------------------------------------------------- + + // Splitting + //----------------------------------------------------------------------- + + /** + *

Splits the provided text into an array, using whitespace as the + * separator. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.split(null)       = null
+     * StringUtils.split("")         = []
+     * StringUtils.split("abc def")  = ["abc", "def"]
+     * StringUtils.split("abc  def") = ["abc", "def"]
+     * StringUtils.split(" abc ")    = ["abc"]
+     * 
+ * + * @param str the String to parse, may be null + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(final String str) { + return split(str, null, -1); + } + + /** + *

Splits the provided text into an array, separator specified. + * This is an alternative to using StringTokenizer.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
+     * StringUtils.split("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.split("a b c", ' ')    = ["a", "b", "c"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChar the character used as the delimiter + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.0 + */ + public static String[] split(final String str, final char separatorChar) { + return splitWorker(str, separatorChar, false); + } + + /** + *

Splits the provided text into an array, separators specified. + * This is an alternative to using StringTokenizer.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ *

+ *

+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("abc def", null) = ["abc", "def"]
+     * StringUtils.split("abc def", " ")  = ["abc", "def"]
+     * StringUtils.split("abc  def", " ") = ["abc", "def"]
+     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(final String str, final String separatorChars) { + return splitWorker(str, separatorChars, -1, false); + } + + /** + *

Splits the provided text into an array with a maximum length, + * separators specified.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ *

+ *

If more than {@code max} delimited substrings are found, the last + * returned string includes all characters after the first {@code max - 1} + * returned strings (including separator characters).

+ *

+ *

+     * StringUtils.split(null, *, *)            = null
+     * StringUtils.split("", *, *)              = []
+     * StringUtils.split("ab cd ef", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.split("ab   cd ef", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(final String str, final String separatorChars, final int max) { + return splitWorker(str, separatorChars, max, false); + } + + /** + *

Splits the provided text into an array, separator string specified.

+ *

+ *

The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ *

+ *

+     * StringUtils.splitByWholeSeparator(null, *)               = null
+     * StringUtils.splitByWholeSeparator("", *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String was input + */ + public static String[] splitByWholeSeparator(final String str, final String separator) { + return splitByWholeSeparatorWorker(str, separator, -1, false); + } + + /** + *

Splits the provided text into an array, separator string specified. + * Returns a maximum of {@code max} substrings.

+ *

+ *

The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ *

+ *

+     * StringUtils.splitByWholeSeparator(null, *, *)               = null
+     * StringUtils.splitByWholeSeparator("", *, *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null, 0)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null, 0)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @return an array of parsed Strings, {@code null} if null String was input + */ + public static String[] splitByWholeSeparator(final String str, final String separator, final int max) { + return splitByWholeSeparatorWorker(str, separator, max, false); + } + + /** + *

Splits the provided text into an array, separator string specified.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ *

+ *

+     * StringUtils.splitByWholeSeparatorPreserveAllTokens(null, *)               = null
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("", *)                 = []
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab de fg", null)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab   de fg", null)    = ["ab", "", "", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String was input + * @since 2.4 + */ + public static String[] splitByWholeSeparatorPreserveAllTokens(final String str, final String separator) { + return splitByWholeSeparatorWorker(str, separator, -1, true); + } + + /** + *

Splits the provided text into an array, separator string specified. + * Returns a maximum of {@code max} substrings.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ *

+ *

+     * StringUtils.splitByWholeSeparatorPreserveAllTokens(null, *, *)               = null
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("", *, *)                 = []
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab de fg", null, 0)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab   de fg", null, 0)    = ["ab", "", "", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @return an array of parsed Strings, {@code null} if null String was input + * @since 2.4 + */ + public static String[] splitByWholeSeparatorPreserveAllTokens(final String str, final String separator, final int max) { + return splitByWholeSeparatorWorker(str, separator, max, true); + } + + /** + * Performs the logic for the {@code splitByWholeSeparatorPreserveAllTokens} methods. + * + * @param str the String to parse, may be {@code null} + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + private static String[] splitByWholeSeparatorWorker( + final String str, final String separator, final int max, final boolean preserveAllTokens) { + if (str == null) { + return null; + } + + final int len = str.length(); + + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + + if (separator == null || EMPTY.equals(separator)) { + // Split on whitespace. + return splitWorker(str, null, max, preserveAllTokens); + } + + final int separatorLength = separator.length(); + + final ArrayList substrings = new ArrayList(); + int numberOfSubstrings = 0; + int beg = 0; + int end = 0; + while (end < len) { + end = str.indexOf(separator, beg); + + if (end > -1) { + if (end > beg) { + numberOfSubstrings += 1; + + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + // The following is OK, because String.substring( beg, end ) excludes + // the character at the position 'end'. + substrings.add(str.substring(beg, end)); + + // Set the starting point for the next search. + // The following is equivalent to beg = end + (separatorLength - 1) + 1, + // which is the right calculation: + beg = end + separatorLength; + } + } else { + // We found a consecutive occurrence of the separator, so skip it. + if (preserveAllTokens) { + numberOfSubstrings += 1; + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + substrings.add(EMPTY); + } + } + beg = end + separatorLength; + } + } else { + // String.substring( beg ) goes from 'beg' to the end of the String. + substrings.add(str.substring(beg)); + end = len; + } + } + + return substrings.toArray(new String[substrings.size()]); + } + + // ----------------------------------------------------------------------- + + /** + *

Splits the provided text into an array, using whitespace as the + * separator, preserving all tokens, including empty tokens created by + * adjacent separators. This is an alternative to using StringTokenizer. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.splitPreserveAllTokens(null)       = null
+     * StringUtils.splitPreserveAllTokens("")         = []
+     * StringUtils.splitPreserveAllTokens("abc def")  = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc  def") = ["abc", "", "def"]
+     * StringUtils.splitPreserveAllTokens(" abc ")    = ["", "abc", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(final String str) { + return splitWorker(str, null, -1, true); + } + + /** + *

Splits the provided text into an array, separator specified, + * preserving all tokens, including empty tokens created by adjacent + * separators. This is an alternative to using StringTokenizer.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.splitPreserveAllTokens(null, *)         = null
+     * StringUtils.splitPreserveAllTokens("", *)           = []
+     * StringUtils.splitPreserveAllTokens("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a..b.c", '.')   = ["a", "", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.splitPreserveAllTokens("a\tb\nc", null) = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a b c", ' ')    = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a b c ", ' ')   = ["a", "b", "c", ""]
+     * StringUtils.splitPreserveAllTokens("a b c  ", ' ')   = ["a", "b", "c", "", ""]
+     * StringUtils.splitPreserveAllTokens(" a b c", ' ')   = ["", a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("  a b c", ' ')  = ["", "", a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens(" a b c ", ' ')  = ["", a", "b", "c", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChar the character used as the delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(final String str, final char separatorChar) { + return splitWorker(str, separatorChar, true); + } + + /** + * Performs the logic for the {@code split} and + * {@code splitPreserveAllTokens} methods that do not return a + * maximum array length. + * + * @param str the String to parse, may be {@code null} + * @param separatorChar the separate character + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + */ + private static String[] splitWorker(final String str, final char separatorChar, final boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + final int len = str.length(); + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + final List list = new ArrayList(); + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + while (i < len) { + if (str.charAt(i) == separatorChar) { + if (match || preserveAllTokens) { + list.add(str.substring(start, i)); + match = false; + lastMatch = true; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + if (match || preserveAllTokens && lastMatch) { + list.add(str.substring(start, i)); + } + return list.toArray(new String[list.size()]); + } + + /** + *

Splits the provided text into an array, separators specified, + * preserving all tokens, including empty tokens created by adjacent + * separators. This is an alternative to using StringTokenizer.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ *

+ *

+     * StringUtils.splitPreserveAllTokens(null, *)           = null
+     * StringUtils.splitPreserveAllTokens("", *)             = []
+     * StringUtils.splitPreserveAllTokens("abc def", null)   = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc def", " ")    = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc  def", " ")   = ["abc", "", def"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":")   = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef:", ":")  = ["ab", "cd", "ef", ""]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef::", ":") = ["ab", "cd", "ef", "", ""]
+     * StringUtils.splitPreserveAllTokens("ab::cd:ef", ":")  = ["ab", "", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens(":cd:ef", ":")     = ["", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("::cd:ef", ":")    = ["", "", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens(":cd:ef:", ":")    = ["", cd", "ef", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(final String str, final String separatorChars) { + return splitWorker(str, separatorChars, -1, true); + } + + /** + *

Splits the provided text into an array with a maximum length, + * separators specified, preserving all tokens, including empty tokens + * created by adjacent separators.

+ *

+ *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * Adjacent separators are treated as one separator.

+ *

+ *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ *

+ *

If more than {@code max} delimited substrings are found, the last + * returned string includes all characters after the first {@code max - 1} + * returned strings (including separator characters).

+ *

+ *

+     * StringUtils.splitPreserveAllTokens(null, *, *)            = null
+     * StringUtils.splitPreserveAllTokens("", *, *)              = []
+     * StringUtils.splitPreserveAllTokens("ab de fg", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 2) = ["ab", "  de fg"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 3) = ["ab", "", " de fg"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 4) = ["ab", "", "", "de fg"]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(final String str, final String separatorChars, final int max) { + return splitWorker(str, separatorChars, max, true); + } + + /** + * Performs the logic for the {@code split} and + * {@code splitPreserveAllTokens} methods that return a maximum array + * length. + * + * @param str the String to parse, may be {@code null} + * @param separatorChars the separate character + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit. + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + */ + private static String[] splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + // Direct code is quicker than StringTokenizer. + // Also, StringTokenizer uses isSpace() not isWhitespace() + + if (str == null) { + return null; + } + final int len = str.length(); + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + final List list = new ArrayList(); + int sizePlus1 = 1; + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + if (separatorChars == null) { + // Null separator means use whitespace + while (i < len) { + if (Character.isWhitespace(str.charAt(i))) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else if (separatorChars.length() == 1) { + // Optimise 1 character case + final char sep = separatorChars.charAt(0); + while (i < len) { + if (str.charAt(i) == sep) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else { + // standard case + while (i < len) { + if (separatorChars.indexOf(str.charAt(i)) >= 0) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } + if (match || preserveAllTokens && lastMatch) { + list.add(str.substring(start, i)); + } + return list.toArray(new String[list.size()]); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens. + *

+     * StringUtils.splitByCharacterType(null)         = null
+     * StringUtils.splitByCharacterType("")           = []
+     * StringUtils.splitByCharacterType("ab de fg")   = ["ab", " ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterType("ab   de fg") = ["ab", "   ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterType("ab:cd:ef")   = ["ab", ":", "cd", ":", "ef"]
+     * StringUtils.splitByCharacterType("number5")    = ["number", "5"]
+     * StringUtils.splitByCharacterType("fooBar")     = ["foo", "B", "ar"]
+     * StringUtils.splitByCharacterType("foo200Bar")  = ["foo", "200", "B", "ar"]
+     * StringUtils.splitByCharacterType("ASFRules")   = ["ASFR", "ules"]
+     * 
+ * + * @param str the String to split, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + public static String[] splitByCharacterType(final String str) { + return splitByCharacterType(str, false); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens, with the + * following exception: the character of type + * {@code Character.UPPERCASE_LETTER}, if any, immediately + * preceding a token of type {@code Character.LOWERCASE_LETTER} + * will belong to the following token rather than to the preceding, if any, + * {@code Character.UPPERCASE_LETTER} token. + *

+     * StringUtils.splitByCharacterTypeCamelCase(null)         = null
+     * StringUtils.splitByCharacterTypeCamelCase("")           = []
+     * StringUtils.splitByCharacterTypeCamelCase("ab de fg")   = ["ab", " ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterTypeCamelCase("ab   de fg") = ["ab", "   ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterTypeCamelCase("ab:cd:ef")   = ["ab", ":", "cd", ":", "ef"]
+     * StringUtils.splitByCharacterTypeCamelCase("number5")    = ["number", "5"]
+     * StringUtils.splitByCharacterTypeCamelCase("fooBar")     = ["foo", "Bar"]
+     * StringUtils.splitByCharacterTypeCamelCase("foo200Bar")  = ["foo", "200", "Bar"]
+     * StringUtils.splitByCharacterTypeCamelCase("ASFRules")   = ["ASF", "Rules"]
+     * 
+ * + * @param str the String to split, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + public static String[] splitByCharacterTypeCamelCase(final String str) { + return splitByCharacterType(str, true); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens, with the + * following exception: if {@code camelCase} is {@code true}, + * the character of type {@code Character.UPPERCASE_LETTER}, if any, + * immediately preceding a token of type {@code Character.LOWERCASE_LETTER} + * will belong to the following token rather than to the preceding, if any, + * {@code Character.UPPERCASE_LETTER} token. + * + * @param str the String to split, may be {@code null} + * @param camelCase whether to use so-called "camel-case" for letter types + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + private static String[] splitByCharacterType(final String str, final boolean camelCase) { + if (str == null) { + return null; + } + if (str.isEmpty()) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + final char[] c = str.toCharArray(); + final List list = new ArrayList(); + int tokenStart = 0; + int currentType = Character.getType(c[tokenStart]); + for (int pos = tokenStart + 1; pos < c.length; pos++) { + final int type = Character.getType(c[pos]); + if (type == currentType) { + continue; + } + if (camelCase && type == Character.LOWERCASE_LETTER && currentType == Character.UPPERCASE_LETTER) { + final int newTokenStart = pos - 1; + if (newTokenStart != tokenStart) { + list.add(new String(c, tokenStart, newTokenStart - tokenStart)); + tokenStart = newTokenStart; + } + } else { + list.add(new String(c, tokenStart, pos - tokenStart)); + tokenStart = pos; + } + currentType = type; + } + list.add(new String(c, tokenStart, c.length - tokenStart)); + return list.toArray(new String[list.size()]); + } + + // Joining + //----------------------------------------------------------------------- + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ *

+ *

No separator is added to the joined String. + * Null objects or empty strings within the array are represented by + * empty strings.

+ *

+ *

+     * StringUtils.join(null)            = null
+     * StringUtils.join([])              = ""
+     * StringUtils.join([null])          = ""
+     * StringUtils.join(["a", "b", "c"]) = "abc"
+     * StringUtils.join([null, "", "a"]) = "a"
+     * 
+ * + * @param the specific type of values to join together + * @param elements the values to join together, may be null + * @return the joined String, {@code null} if null array input + * @since 3.0 Changed signature to use varargs + */ + public static String join(final T... elements) { + return join(elements, null); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ *

+ *

No delimiter is added before or after the list. + * Null objects or empty strings within the array are represented by + * empty strings.

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join(["a", "b", "c"], ';')  = "a;b;c"
+     * StringUtils.join(["a", "b", "c"], null) = "abc"
+     * StringUtils.join([null, "", "a"], ';')  = ";;a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 2.0 + */ + public static String join(final Object[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final long[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final int[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final short[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final byte[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final char[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final float[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final double[] array, final char separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ *

+ *

No delimiter is added before or after the list. + * Null objects or empty strings within the array are represented by + * empty strings.

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join(["a", "b", "c"], ';')  = "a;b;c"
+     * StringUtils.join(["a", "b", "c"], null) = "abc"
+     * StringUtils.join([null, "", "a"], ';')  = ";;a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is + * an error to pass in an end index past the end of the array + * @param endIndex the index to stop joining from (exclusive). It is + * an error to pass in an end index past the end of the array + * @return the joined String, {@code null} if null array input + * @since 2.0 + */ + public static String join(final Object[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final long[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final int[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final byte[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final short[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final char[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final double[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of elements. + *

+ *

+ *

+ * No delimiter is added before or after the list. Null objects or empty strings within the array are represented + * by empty strings. + *

+ *

+ *

+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join([1, 2, 3], ';')  = "1;2;3"
+     * StringUtils.join([1, 2, 3], null) = "123"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is an error to pass in an end index past the end of the + * array + * @param endIndex the index to stop joining from (exclusive). It is an error to pass in an end index past the end of + * the array + * @return the joined String, {@code null} if null array input + * @since 3.2 + */ + public static String join(final float[] array, final char separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder buf = new StringBuilder(noOfItems * 16); + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ *

+ *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String (""). + * Null objects or empty strings within the array are represented by + * empty strings.

+ *

+ *

+     * StringUtils.join(null, *)                = null
+     * StringUtils.join([], *)                  = ""
+     * StringUtils.join([null], *)              = ""
+     * StringUtils.join(["a", "b", "c"], "--")  = "a--b--c"
+     * StringUtils.join(["a", "b", "c"], null)  = "abc"
+     * StringUtils.join(["a", "b", "c"], "")    = "abc"
+     * StringUtils.join([null, "", "a"], ',')   = ",,a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null array input + */ + public static String join(final Object[] array, final String separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ *

+ *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String (""). + * Null objects or empty strings within the array are represented by + * empty strings.

+ *

+ *

+     * StringUtils.join(null, *, *, *)                = null
+     * StringUtils.join([], *, *, *)                  = ""
+     * StringUtils.join([null], *, *, *)              = ""
+     * StringUtils.join(["a", "b", "c"], "--", 0, 3)  = "a--b--c"
+     * StringUtils.join(["a", "b", "c"], "--", 1, 3)  = "b--c"
+     * StringUtils.join(["a", "b", "c"], "--", 2, 3)  = "c"
+     * StringUtils.join(["a", "b", "c"], "--", 2, 2)  = ""
+     * StringUtils.join(["a", "b", "c"], null, 0, 3)  = "abc"
+     * StringUtils.join(["a", "b", "c"], "", 0, 3)    = "abc"
+     * StringUtils.join([null, "", "a"], ',', 0, 3)   = ",,a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @param startIndex the first index to start joining from. + * @param endIndex the index to stop joining from (exclusive). + * @return the joined String, {@code null} if null array input; or the empty string + * if {@code endIndex - startIndex <= 0}. The number of joined entries is given by + * {@code endIndex - startIndex} + * @throws ArrayIndexOutOfBoundsException ife
+ * {@code startIndex < 0} or
+ * {@code startIndex >= array.length()} or
+ * {@code endIndex < 0} or
+ * {@code endIndex > array.length()} + */ + public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + if (separator == null) { + separator = EMPTY; + } + + // endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator)) + // (Assuming that all Strings are roughly equally long) + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + + final StringBuilder buf = new StringBuilder(noOfItems * 16); + + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterator} into + * a single String containing the provided elements.

+ *

+ *

No delimiter is added before or after the list. Null objects or empty + * strings within the iteration are represented by empty strings.

+ *

+ *

See the examples here: {@link #join(Object[], char)}.

+ * + * @param iterator the {@code Iterator} of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null iterator input + * @since 2.0 + */ + public static String join(final Iterator iterator, final char separator) { + + // handle null, zero and one elements before building a buffer + if (iterator == null) { + return null; + } + if (!iterator.hasNext()) { + return EMPTY; + } + final Object first = iterator.next(); + if (!iterator.hasNext()) { + @SuppressWarnings("deprecation") // ObjectUtils.toString(Object) has been deprecated in 3.2 + String result = ObjectUtils.toString(first); + return result; + } + + // two or more elements + final StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small + if (first != null) { + buf.append(first); + } + + while (iterator.hasNext()) { + buf.append(separator); + final Object obj = iterator.next(); + if (obj != null) { + buf.append(obj); + } + } + + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterator} into + * a single String containing the provided elements.

+ *

+ *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String ("").

+ *

+ *

See the examples here: {@link #join(Object[], String)}.

+ * + * @param iterator the {@code Iterator} of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null iterator input + */ + public static String join(final Iterator iterator, final String separator) { + + // handle null, zero and one elements before building a buffer + if (iterator == null) { + return null; + } + if (!iterator.hasNext()) { + return EMPTY; + } + final Object first = iterator.next(); + if (!iterator.hasNext()) { + @SuppressWarnings("deprecation") // ObjectUtils.toString(Object) has been deprecated in 3.2 + final String result = ObjectUtils.toString(first); + return result; + } + + // two or more elements + final StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small + if (first != null) { + buf.append(first); + } + + while (iterator.hasNext()) { + if (separator != null) { + buf.append(separator); + } + final Object obj = iterator.next(); + if (obj != null) { + buf.append(obj); + } + } + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterable} into + * a single String containing the provided elements.

+ *

+ *

No delimiter is added before or after the list. Null objects or empty + * strings within the iteration are represented by empty strings.

+ *

+ *

See the examples here: {@link #join(Object[], char)}.

+ * + * @param iterable the {@code Iterable} providing the values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null iterator input + * @since 2.3 + */ + public static String join(final Iterable iterable, final char separator) { + if (iterable == null) { + return null; + } + return join(iterable.iterator(), separator); + } + + /** + *

Joins the elements of the provided {@code Iterable} into + * a single String containing the provided elements.

+ *

+ *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String ("").

+ *

+ *

See the examples here: {@link #join(Object[], String)}.

+ * + * @param iterable the {@code Iterable} providing the values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null iterator input + * @since 2.3 + */ + public static String join(final Iterable iterable, final String separator) { + if (iterable == null) { + return null; + } + return join(iterable.iterator(), separator); + } + + // Delete + //----------------------------------------------------------------------- + + /** + *

Deletes all whitespaces from a String as defined by + * {@link Character#isWhitespace(char)}.

+ *

+ *

+     * StringUtils.deleteWhitespace(null)         = null
+     * StringUtils.deleteWhitespace("")           = ""
+     * StringUtils.deleteWhitespace("abc")        = "abc"
+     * StringUtils.deleteWhitespace("   ab  c  ") = "abc"
+     * 
+ * + * @param str the String to delete whitespace from, may be null + * @return the String without whitespaces, {@code null} if null String input + */ + public static String deleteWhitespace(final String str) { + if (isEmpty(str)) { + return str; + } + final int sz = str.length(); + final char[] chs = new char[sz]; + int count = 0; + for (int i = 0; i < sz; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + chs[count++] = str.charAt(i); + } + } + if (count == sz) { + return str; + } + return new String(chs, 0, count); + } + + // Remove + //----------------------------------------------------------------------- + + /** + *

Removes a substring only if it is at the beginning of a source string, + * otherwise returns the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ *

+ *

+     * StringUtils.removeStart(null, *)      = null
+     * StringUtils.removeStart("", *)        = ""
+     * StringUtils.removeStart(*, null)      = *
+     * StringUtils.removeStart("www.domain.com", "www.")   = "domain.com"
+     * StringUtils.removeStart("domain.com", "www.")       = "domain.com"
+     * StringUtils.removeStart("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeStart("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String removeStart(final String str, final String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (str.startsWith(remove)) { + return str.substring(remove.length()); + } + return str; + } + + /** + *

Case insensitive removal of a substring if it is at the beginning of a source string, + * otherwise returns the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ *

+ *

+     * StringUtils.removeStartIgnoreCase(null, *)      = null
+     * StringUtils.removeStartIgnoreCase("", *)        = ""
+     * StringUtils.removeStartIgnoreCase(*, null)      = *
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "www.")   = "domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "WWW.")   = "domain.com"
+     * StringUtils.removeStartIgnoreCase("domain.com", "www.")       = "domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeStartIgnoreCase("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for (case insensitive) and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.4 + */ + public static String removeStartIgnoreCase(final String str, final String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (startsWithIgnoreCase(str, remove)) { + return str.substring(remove.length()); + } + return str; + } + + /** + *

Removes a substring only if it is at the end of a source string, + * otherwise returns the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ *

+ *

+     * StringUtils.removeEnd(null, *)      = null
+     * StringUtils.removeEnd("", *)        = ""
+     * StringUtils.removeEnd(*, null)      = *
+     * StringUtils.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
+     * StringUtils.removeEnd("www.domain.com", ".com")   = "www.domain"
+     * StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeEnd("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String removeEnd(final String str, final String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (str.endsWith(remove)) { + return str.substring(0, str.length() - remove.length()); + } + return str; + } + + /** + *

Case insensitive removal of a substring if it is at the end of a source string, + * otherwise returns the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ *

+ *

+     * StringUtils.removeEndIgnoreCase(null, *)      = null
+     * StringUtils.removeEndIgnoreCase("", *)        = ""
+     * StringUtils.removeEndIgnoreCase(*, null)      = *
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com.")  = "www.domain.com"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com")   = "www.domain"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeEndIgnoreCase("abc", "")    = "abc"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".COM") = "www.domain")
+     * StringUtils.removeEndIgnoreCase("www.domain.COM", ".com") = "www.domain")
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for (case insensitive) and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.4 + */ + public static String removeEndIgnoreCase(final String str, final String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (endsWithIgnoreCase(str, remove)) { + return str.substring(0, str.length() - remove.length()); + } + return str; + } + + /** + *

Removes all occurrences of a substring from within the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} remove string will return the source string. + * An empty ("") remove string will return the source string.

+ *

+ *

+     * StringUtils.remove(null, *)        = null
+     * StringUtils.remove("", *)          = ""
+     * StringUtils.remove(*, null)        = *
+     * StringUtils.remove(*, "")          = *
+     * StringUtils.remove("queued", "ue") = "qd"
+     * StringUtils.remove("queued", "zz") = "queued"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String remove(final String str, final String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + return replace(str, remove, EMPTY, -1); + } + + /** + *

Removes all occurrences of a character from within the source string.

+ *

+ *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string.

+ *

+ *

+     * StringUtils.remove(null, *)       = null
+     * StringUtils.remove("", *)         = ""
+     * StringUtils.remove("queued", 'u') = "qeed"
+     * StringUtils.remove("queued", 'z') = "queued"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the char to search for and remove, may be null + * @return the substring with the char removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String remove(final String str, final char remove) { + if (isEmpty(str) || str.indexOf(remove) == INDEX_NOT_FOUND) { + return str; + } + final char[] chars = str.toCharArray(); + int pos = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] != remove) { + chars[pos++] = chars[i]; + } + } + return new String(chars, 0, pos); + } + + // Replacing + //----------------------------------------------------------------------- + + /** + *

Replaces a String with another String inside a larger String, once.

+ *

+ *

A {@code null} reference passed to this method is a no-op.

+ *

+ *

+     * StringUtils.replaceOnce(null, *, *)        = null
+     * StringUtils.replaceOnce("", *, *)          = ""
+     * StringUtils.replaceOnce("any", null, *)    = "any"
+     * StringUtils.replaceOnce("any", *, null)    = "any"
+     * StringUtils.replaceOnce("any", "", *)      = "any"
+     * StringUtils.replaceOnce("aba", "a", null)  = "aba"
+     * StringUtils.replaceOnce("aba", "a", "")    = "ba"
+     * StringUtils.replaceOnce("aba", "a", "z")   = "zba"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace with, may be null + * @return the text with any replacements processed, + * {@code null} if null String input + * @see #replace(String text, String searchString, String replacement, int max) + */ + public static String replaceOnce(final String text, final String searchString, final String replacement) { + return replace(text, searchString, replacement, 1); + } + + /** + * Replaces each substring of the source String that matches the given regular expression with the given + * replacement using the {@link Pattern#DOTALL} option. DOTALL is also know as single-line mode in Perl. This call + * is also equivalent to: + *
    + *
  • {@code source.replaceAll("(?s)" + regex, replacement)}
  • + *
  • {@code Pattern.compile(regex, Pattern.DOTALL).matcher(source).replaceAll(replacement)}
  • + *
+ * + * @param source the source string + * @param regex the regular expression to which this string is to be matched + * @param replacement the string to be substituted for each match + * @return The resulting {@code String} + * @see String#replaceAll(String, String) + * @see Pattern#DOTALL + * @since 3.2 + */ + public static String replacePattern(final String source, final String regex, final String replacement) { + return Pattern.compile(regex, Pattern.DOTALL).matcher(source).replaceAll(replacement); + } + + /** + * Removes each substring of the source String that matches the given regular expression using the DOTALL option. + * + * @param source the source string + * @param regex the regular expression to which this string is to be matched + * @return The resulting {@code String} + * @see String#replaceAll(String, String) + * @see Pattern#DOTALL + * @since 3.2 + */ + public static String removePattern(final String source, final String regex) { + return replacePattern(source, regex, StringUtils.EMPTY); + } + + /** + *

Replaces all occurrences of a String within another String.

+ *

+ *

A {@code null} reference passed to this method is a no-op.

+ *

+ *

+     * StringUtils.replace(null, *, *)        = null
+     * StringUtils.replace("", *, *)          = ""
+     * StringUtils.replace("any", null, *)    = "any"
+     * StringUtils.replace("any", *, null)    = "any"
+     * StringUtils.replace("any", "", *)      = "any"
+     * StringUtils.replace("aba", "a", null)  = "aba"
+     * StringUtils.replace("aba", "a", "")    = "b"
+     * StringUtils.replace("aba", "a", "z")   = "zbz"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace it with, may be null + * @return the text with any replacements processed, + * {@code null} if null String input + * @see #replace(String text, String searchString, String replacement, int max) + */ + public static String replace(final String text, final String searchString, final String replacement) { + return replace(text, searchString, replacement, -1); + } + + /** + *

Replaces a String with another String inside a larger String, + * for the first {@code max} values of the search String.

+ *

+ *

A {@code null} reference passed to this method is a no-op.

+ *

+ *

+     * StringUtils.replace(null, *, *, *)         = null
+     * StringUtils.replace("", *, *, *)           = ""
+     * StringUtils.replace("any", null, *, *)     = "any"
+     * StringUtils.replace("any", *, null, *)     = "any"
+     * StringUtils.replace("any", "", *, *)       = "any"
+     * StringUtils.replace("any", *, *, 0)        = "any"
+     * StringUtils.replace("abaa", "a", null, -1) = "abaa"
+     * StringUtils.replace("abaa", "a", "", -1)   = "b"
+     * StringUtils.replace("abaa", "a", "z", 0)   = "abaa"
+     * StringUtils.replace("abaa", "a", "z", 1)   = "zbaa"
+     * StringUtils.replace("abaa", "a", "z", 2)   = "zbza"
+     * StringUtils.replace("abaa", "a", "z", -1)  = "zbzz"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace it with, may be null + * @param max maximum number of values to replace, or {@code -1} if no maximum + * @return the text with any replacements processed, + * {@code null} if null String input + */ + public static String replace(final String text, final String searchString, final String replacement, int max) { + if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) { + return text; + } + int start = 0; + int end = text.indexOf(searchString, start); + if (end == INDEX_NOT_FOUND) { + return text; + } + final int replLength = searchString.length(); + int increase = replacement.length() - replLength; + increase = increase < 0 ? 0 : increase; + increase *= max < 0 ? 16 : max > 64 ? 64 : max; + final StringBuilder buf = new StringBuilder(text.length() + increase); + while (end != INDEX_NOT_FOUND) { + buf.append(text.substring(start, end)).append(replacement); + start = end + replLength; + if (--max == 0) { + break; + } + end = text.indexOf(searchString, start); + } + buf.append(text.substring(start)); + return buf.toString(); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ *

+ *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. This will not repeat. For repeating replaces, call the + * overloaded method. + *

+ *

+ *

+     *  StringUtils.replaceEach(null, *, *)        = null
+     *  StringUtils.replaceEach("", *, *)          = ""
+     *  StringUtils.replaceEach("aba", null, null) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0]) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
+     *  (example of how it does not repeat)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
+     * 
+ * + * @param text text to search and replace in, no-op if null + * @param searchList the Strings to search for, no-op if null + * @param replacementList the Strings to replace them with, no-op if null + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + public static String replaceEach(final String text, final String[] searchList, final String[] replacementList) { + return replaceEach(text, searchList, replacementList, false, 0); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ *

+ *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. + *

+ *

+ *

+     *  StringUtils.replaceEach(null, *, *, *) = null
+     *  StringUtils.replaceEach("", *, *, *) = ""
+     *  StringUtils.replaceEach("aba", null, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
+     *  (example of how it repeats)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, true) = IllegalStateException
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, false) = "dcabe"
+     * 
+ * + * @param text text to search and replace in, no-op if null + * @param searchList the Strings to search for, no-op if null + * @param replacementList the Strings to replace them with, no-op if null + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalStateException if the search is repeating and there is an endless loop due + * to outputs of one being inputs to another + * @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + public static String replaceEachRepeatedly(final String text, final String[] searchList, final String[] replacementList) { + // timeToLive should be 0 if not used or nothing to replace, else it's + // the length of the replace array + final int timeToLive = searchList == null ? 0 : searchList.length; + return replaceEach(text, searchList, replacementList, true, timeToLive); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ *

+ *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. + *

+ *

+ *

+     *  StringUtils.replaceEach(null, *, *, *) = null
+     *  StringUtils.replaceEach("", *, *, *) = ""
+     *  StringUtils.replaceEach("aba", null, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
+     *  (example of how it repeats)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalStateException
+     * 
+ * + * @param text text to search and replace in, no-op if null + * @param searchList the Strings to search for, no-op if null + * @param replacementList the Strings to replace them with, no-op if null + * @param repeat if true, then replace repeatedly + * until there are no more possible replacements or timeToLive < 0 + * @param timeToLive if less than 0 then there is a circular reference and endless + * loop + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalStateException if the search is repeating and there is an endless loop due + * to outputs of one being inputs to another + * @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + private static String replaceEach( + final String text, final String[] searchList, final String[] replacementList, final boolean repeat, final int timeToLive) { + + // mchyzer Performance note: This creates very few new objects (one major goal) + // let me know if there are performance requests, we can create a harness to measure + + if (text == null || text.isEmpty() || searchList == null || + searchList.length == 0 || replacementList == null || replacementList.length == 0) { + return text; + } + + // if recursing, this shouldn't be less than 0 + if (timeToLive < 0) { + throw new IllegalStateException("Aborting to protect against StackOverflowError - " + + "output of one loop is the input of another"); + } + + final int searchLength = searchList.length; + final int replacementLength = replacementList.length; + + // make sure lengths are ok, these need to be equal + if (searchLength != replacementLength) { + throw new IllegalArgumentException("Search and Replace array lengths don't match: " + + searchLength + + " vs " + + replacementLength); + } + + // keep track of which still have matches + final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength]; + + // index on index that the match was found + int textIndex = -1; + int replaceIndex = -1; + int tempIndex = -1; + + // index of replace array that will replace the search string found + // NOTE: logic duplicated below START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].isEmpty() || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i]); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic mostly below END + + // no search strings found, we are done + if (textIndex == -1) { + return text; + } + + int start = 0; + + // get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit + int increase = 0; + + // count the replacement text elements that are larger than their corresponding text being replaced + for (int i = 0; i < searchList.length; i++) { + if (searchList[i] == null || replacementList[i] == null) { + continue; + } + final int greater = replacementList[i].length() - searchList[i].length(); + if (greater > 0) { + increase += 3 * greater; // assume 3 matches + } + } + // have upper-bound at 20% increase, then let Java take over + increase = Math.min(increase, text.length() / 5); + + final StringBuilder buf = new StringBuilder(text.length() + increase); + + while (textIndex != -1) { + + for (int i = start; i < textIndex; i++) { + buf.append(text.charAt(i)); + } + buf.append(replacementList[replaceIndex]); + + start = textIndex + searchList[replaceIndex].length(); + + textIndex = -1; + replaceIndex = -1; + tempIndex = -1; + // find the next earliest match + // NOTE: logic mostly duplicated above START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].isEmpty() || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i], start); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic duplicated above END + + } + final int textLength = text.length(); + for (int i = start; i < textLength; i++) { + buf.append(text.charAt(i)); + } + final String result = buf.toString(); + if (!repeat) { + return result; + } + + return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1); + } + + // Replace, character based + //----------------------------------------------------------------------- + + /** + *

Replaces all occurrences of a character in a String with another. + * This is a null-safe version of {@link String#replace(char, char)}.

+ *

+ *

A {@code null} string input returns {@code null}. + * An empty ("") string input returns an empty string.

+ *

+ *

+     * StringUtils.replaceChars(null, *, *)        = null
+     * StringUtils.replaceChars("", *, *)          = ""
+     * StringUtils.replaceChars("abcba", 'b', 'y') = "aycya"
+     * StringUtils.replaceChars("abcba", 'z', 'y') = "abcba"
+     * 
+ * + * @param str String to replace characters in, may be null + * @param searchChar the character to search for, may be null + * @param replaceChar the character to replace, may be null + * @return modified String, {@code null} if null string input + * @since 2.0 + */ + public static String replaceChars(final String str, final char searchChar, final char replaceChar) { + if (str == null) { + return null; + } + return str.replace(searchChar, replaceChar); + } + + /** + *

Replaces multiple characters in a String in one go. + * This method can also be used to delete characters.

+ *

+ *

For example:
+ * replaceChars("hello", "ho", "jy") = jelly.

+ *

+ *

A {@code null} string input returns {@code null}. + * An empty ("") string input returns an empty string. + * A null or empty set of search characters returns the input string.

+ *

+ *

The length of the search characters should normally equal the length + * of the replace characters. + * If the search characters is longer, then the extra search characters + * are deleted. + * If the search characters is shorter, then the extra replace characters + * are ignored.

+ *

+ *

+     * StringUtils.replaceChars(null, *, *)           = null
+     * StringUtils.replaceChars("", *, *)             = ""
+     * StringUtils.replaceChars("abc", null, *)       = "abc"
+     * StringUtils.replaceChars("abc", "", *)         = "abc"
+     * StringUtils.replaceChars("abc", "b", null)     = "ac"
+     * StringUtils.replaceChars("abc", "b", "")       = "ac"
+     * StringUtils.replaceChars("abcba", "bc", "yz")  = "ayzya"
+     * StringUtils.replaceChars("abcba", "bc", "y")   = "ayya"
+     * StringUtils.replaceChars("abcba", "bc", "yzx") = "ayzya"
+     * 
+ * + * @param str String to replace characters in, may be null + * @param searchChars a set of characters to search for, may be null + * @param replaceChars a set of characters to replace, may be null + * @return modified String, {@code null} if null string input + * @since 2.0 + */ + public static String replaceChars(final String str, final String searchChars, String replaceChars) { + if (isEmpty(str) || isEmpty(searchChars)) { + return str; + } + if (replaceChars == null) { + replaceChars = EMPTY; + } + boolean modified = false; + final int replaceCharsLength = replaceChars.length(); + final int strLength = str.length(); + final StringBuilder buf = new StringBuilder(strLength); + for (int i = 0; i < strLength; i++) { + final char ch = str.charAt(i); + final int index = searchChars.indexOf(ch); + if (index >= 0) { + modified = true; + if (index < replaceCharsLength) { + buf.append(replaceChars.charAt(index)); + } + } else { + buf.append(ch); + } + } + if (modified) { + return buf.toString(); + } + return str; + } + + // Overlay + //----------------------------------------------------------------------- + + /** + *

Overlays part of a String with another String.

+ *

+ *

A {@code null} string input returns {@code null}. + * A negative index is treated as zero. + * An index greater than the string length is treated as the string length. + * The start index is always the smaller of the two indices.

+ *

+ *

+     * StringUtils.overlay(null, *, *, *)            = null
+     * StringUtils.overlay("", "abc", 0, 0)          = "abc"
+     * StringUtils.overlay("abcdef", null, 2, 4)     = "abef"
+     * StringUtils.overlay("abcdef", "", 2, 4)       = "abef"
+     * StringUtils.overlay("abcdef", "", 4, 2)       = "abef"
+     * StringUtils.overlay("abcdef", "zzzz", 2, 4)   = "abzzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", 4, 2)   = "abzzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", -1, 4)  = "zzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", 2, 8)   = "abzzzz"
+     * StringUtils.overlay("abcdef", "zzzz", -2, -3) = "zzzzabcdef"
+     * StringUtils.overlay("abcdef", "zzzz", 8, 10)  = "abcdefzzzz"
+     * 
+ * + * @param str the String to do overlaying in, may be null + * @param overlay the String to overlay, may be null + * @param start the position to start overlaying at + * @param end the position to stop overlaying before + * @return overlayed String, {@code null} if null String input + * @since 2.0 + */ + public static String overlay(final String str, String overlay, int start, int end) { + if (str == null) { + return null; + } + if (overlay == null) { + overlay = EMPTY; + } + final int len = str.length(); + if (start < 0) { + start = 0; + } + if (start > len) { + start = len; + } + if (end < 0) { + end = 0; + } + if (end > len) { + end = len; + } + if (start > end) { + final int temp = start; + start = end; + end = temp; + } + return new StringBuilder(len + start - end + overlay.length() + 1) + .append(str.substring(0, start)) + .append(overlay) + .append(str.substring(end)) + .toString(); + } + + // Chomping + //----------------------------------------------------------------------- + + /** + *

Removes one newline from end of a String if it's there, + * otherwise leave it alone. A newline is "{@code \n}", + * "{@code \r}", or "{@code \r\n}".

+ *

+ *

NOTE: This method changed in 2.0. + * It now more closely matches Perl chomp.

+ *

+ *

+     * StringUtils.chomp(null)          = null
+     * StringUtils.chomp("")            = ""
+     * StringUtils.chomp("abc \r")      = "abc "
+     * StringUtils.chomp("abc\n")       = "abc"
+     * StringUtils.chomp("abc\r\n")     = "abc"
+     * StringUtils.chomp("abc\r\n\r\n") = "abc\r\n"
+     * StringUtils.chomp("abc\n\r")     = "abc\n"
+     * StringUtils.chomp("abc\n\rabc")  = "abc\n\rabc"
+     * StringUtils.chomp("\r")          = ""
+     * StringUtils.chomp("\n")          = ""
+     * StringUtils.chomp("\r\n")        = ""
+     * 
+ * + * @param str the String to chomp a newline from, may be null + * @return String without newline, {@code null} if null String input + */ + public static String chomp(final String str) { + if (isEmpty(str)) { + return str; + } + + if (str.length() == 1) { + final char ch = str.charAt(0); + if (ch == CharUtils.CR || ch == CharUtils.LF) { + return EMPTY; + } + return str; + } + + int lastIdx = str.length() - 1; + final char last = str.charAt(lastIdx); + + if (last == CharUtils.LF) { + if (str.charAt(lastIdx - 1) == CharUtils.CR) { + lastIdx--; + } + } else if (last != CharUtils.CR) { + lastIdx++; + } + return str.substring(0, lastIdx); + } + + /** + *

Removes {@code separator} from the end of + * {@code str} if it's there, otherwise leave it alone.

+ *

+ *

NOTE: This method changed in version 2.0. + * It now more closely matches Perl chomp. + * For the previous behavior, use {@link #substringBeforeLast(String, String)}. + * This method uses {@link String#endsWith(String)}.

+ *

+ *

+     * StringUtils.chomp(null, *)         = null
+     * StringUtils.chomp("", *)           = ""
+     * StringUtils.chomp("foobar", "bar") = "foo"
+     * StringUtils.chomp("foobar", "baz") = "foobar"
+     * StringUtils.chomp("foo", "foo")    = ""
+     * StringUtils.chomp("foo ", "foo")   = "foo "
+     * StringUtils.chomp(" foo", "foo")   = " "
+     * StringUtils.chomp("foo", "foooo")  = "foo"
+     * StringUtils.chomp("foo", "")       = "foo"
+     * StringUtils.chomp("foo", null)     = "foo"
+     * 
+ * + * @param str the String to chomp from, may be null + * @param separator separator String, may be null + * @return String without trailing separator, {@code null} if null String input + * @deprecated This feature will be removed in Lang 4.0, use {@link StringUtils#removeEnd(String, String)} instead + */ + @Deprecated + public static String chomp(final String str, final String separator) { + return removeEnd(str, separator); + } + + // Chopping + //----------------------------------------------------------------------- + + /** + *

Remove the last character from a String.

+ *

+ *

If the String ends in {@code \r\n}, then remove both + * of them.

+ *

+ *

+     * StringUtils.chop(null)          = null
+     * StringUtils.chop("")            = ""
+     * StringUtils.chop("abc \r")      = "abc "
+     * StringUtils.chop("abc\n")       = "abc"
+     * StringUtils.chop("abc\r\n")     = "abc"
+     * StringUtils.chop("abc")         = "ab"
+     * StringUtils.chop("abc\nabc")    = "abc\nab"
+     * StringUtils.chop("a")           = ""
+     * StringUtils.chop("\r")          = ""
+     * StringUtils.chop("\n")          = ""
+     * StringUtils.chop("\r\n")        = ""
+     * 
+ * + * @param str the String to chop last character from, may be null + * @return String without last character, {@code null} if null String input + */ + public static String chop(final String str) { + if (str == null) { + return null; + } + final int strLen = str.length(); + if (strLen < 2) { + return EMPTY; + } + final int lastIdx = strLen - 1; + final String ret = str.substring(0, lastIdx); + final char last = str.charAt(lastIdx); + if (last == CharUtils.LF && ret.charAt(lastIdx - 1) == CharUtils.CR) { + return ret.substring(0, lastIdx - 1); + } + return ret; + } + + // Conversion + //----------------------------------------------------------------------- + + // Padding + //----------------------------------------------------------------------- + + /** + *

Repeat a String {@code repeat} times to form a + * new String.

+ *

+ *

+     * StringUtils.repeat(null, 2) = null
+     * StringUtils.repeat("", 0)   = ""
+     * StringUtils.repeat("", 2)   = ""
+     * StringUtils.repeat("a", 3)  = "aaa"
+     * StringUtils.repeat("ab", 2) = "abab"
+     * StringUtils.repeat("a", -2) = ""
+     * 
+ * + * @param str the String to repeat, may be null + * @param repeat number of times to repeat str, negative treated as zero + * @return a new String consisting of the original String repeated, + * {@code null} if null String input + */ + public static String repeat(final String str, final int repeat) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + if (repeat <= 0) { + return EMPTY; + } + final int inputLength = str.length(); + if (repeat == 1 || inputLength == 0) { + return str; + } + if (inputLength == 1 && repeat <= PAD_LIMIT) { + return repeat(str.charAt(0), repeat); + } + + final int outputLength = inputLength * repeat; + switch (inputLength) { + case 1: + return repeat(str.charAt(0), repeat); + case 2: + final char ch0 = str.charAt(0); + final char ch1 = str.charAt(1); + final char[] output2 = new char[outputLength]; + for (int i = repeat * 2 - 2; i >= 0; i--, i--) { + output2[i] = ch0; + output2[i + 1] = ch1; + } + return new String(output2); + default: + final StringBuilder buf = new StringBuilder(outputLength); + for (int i = 0; i < repeat; i++) { + buf.append(str); + } + return buf.toString(); + } + } + + /** + *

Repeat a String {@code repeat} times to form a + * new String, with a String separator injected each time.

+ *

+ *

+     * StringUtils.repeat(null, null, 2) = null
+     * StringUtils.repeat(null, "x", 2)  = null
+     * StringUtils.repeat("", null, 0)   = ""
+     * StringUtils.repeat("", "", 2)     = ""
+     * StringUtils.repeat("", "x", 3)    = "xxx"
+     * StringUtils.repeat("?", ", ", 3)  = "?, ?, ?"
+     * 
+ * + * @param str the String to repeat, may be null + * @param separator the String to inject, may be null + * @param repeat number of times to repeat str, negative treated as zero + * @return a new String consisting of the original String repeated, + * {@code null} if null String input + * @since 2.5 + */ + public static String repeat(final String str, final String separator, final int repeat) { + if (str == null || separator == null) { + return repeat(str, repeat); + } + // given that repeat(String, int) is quite optimized, better to rely on it than try and splice this into it + final String result = repeat(str + separator, repeat); + return removeEnd(result, separator); + } + + /** + *

Returns padding using the specified delimiter repeated + * to a given length.

+ *

+ *

+     * StringUtils.repeat('e', 0)  = ""
+     * StringUtils.repeat('e', 3)  = "eee"
+     * StringUtils.repeat('e', -2) = ""
+     * 
+ *

+ *

Note: this method doesn't not support padding with + * Unicode Supplementary Characters + * as they require a pair of {@code char}s to be represented. + * If you are needing to support full I18N of your applications + * consider using {@link #repeat(String, int)} instead. + *

+ * + * @param ch character to repeat + * @param repeat number of times to repeat char, negative treated as zero + * @return String with repeated character + * @see #repeat(String, int) + */ + public static String repeat(final char ch, final int repeat) { + final char[] buf = new char[repeat]; + for (int i = repeat - 1; i >= 0; i--) { + buf[i] = ch; + } + return new String(buf); + } + + /** + *

Right pad a String with spaces (' ').

+ *

+ *

The String is padded to the size of {@code size}.

+ *

+ *

+     * StringUtils.rightPad(null, *)   = null
+     * StringUtils.rightPad("", 3)     = "   "
+     * StringUtils.rightPad("bat", 3)  = "bat"
+     * StringUtils.rightPad("bat", 5)  = "bat  "
+     * StringUtils.rightPad("bat", 1)  = "bat"
+     * StringUtils.rightPad("bat", -1) = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String rightPad(final String str, final int size) { + return rightPad(str, size, ' '); + } + + /** + *

Right pad a String with a specified character.

+ *

+ *

The String is padded to the size of {@code size}.

+ *

+ *

+     * StringUtils.rightPad(null, *, *)     = null
+     * StringUtils.rightPad("", 3, 'z')     = "zzz"
+     * StringUtils.rightPad("bat", 3, 'z')  = "bat"
+     * StringUtils.rightPad("bat", 5, 'z')  = "batzz"
+     * StringUtils.rightPad("bat", 1, 'z')  = "bat"
+     * StringUtils.rightPad("bat", -1, 'z') = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padChar the character to pad with + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + * @since 2.0 + */ + public static String rightPad(final String str, final int size, final char padChar) { + if (str == null) { + return null; + } + final int pads = size - str.length(); + if (pads <= 0) { + return str; // returns original String when possible + } + if (pads > PAD_LIMIT) { + return rightPad(str, size, String.valueOf(padChar)); + } + return str.concat(repeat(padChar, pads)); + } + + /** + *

Right pad a String with a specified String.

+ *

+ *

The String is padded to the size of {@code size}.

+ *

+ *

+     * StringUtils.rightPad(null, *, *)      = null
+     * StringUtils.rightPad("", 3, "z")      = "zzz"
+     * StringUtils.rightPad("bat", 3, "yz")  = "bat"
+     * StringUtils.rightPad("bat", 5, "yz")  = "batyz"
+     * StringUtils.rightPad("bat", 8, "yz")  = "batyzyzy"
+     * StringUtils.rightPad("bat", 1, "yz")  = "bat"
+     * StringUtils.rightPad("bat", -1, "yz") = "bat"
+     * StringUtils.rightPad("bat", 5, null)  = "bat  "
+     * StringUtils.rightPad("bat", 5, "")    = "bat  "
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padStr the String to pad with, null or empty treated as single space + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String rightPad(final String str, final int size, String padStr) { + if (str == null) { + return null; + } + if (isEmpty(padStr)) { + padStr = SPACE; + } + final int padLen = padStr.length(); + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str; // returns original String when possible + } + if (padLen == 1 && pads <= PAD_LIMIT) { + return rightPad(str, size, padStr.charAt(0)); + } + + if (pads == padLen) { + return str.concat(padStr); + } else if (pads < padLen) { + return str.concat(padStr.substring(0, pads)); + } else { + final char[] padding = new char[pads]; + final char[] padChars = padStr.toCharArray(); + for (int i = 0; i < pads; i++) { + padding[i] = padChars[i % padLen]; + } + return str.concat(new String(padding)); + } + } + + /** + *

Left pad a String with spaces (' ').

+ *

+ *

The String is padded to the size of {@code size}.

+ *

+ *

+     * StringUtils.leftPad(null, *)   = null
+     * StringUtils.leftPad("", 3)     = "   "
+     * StringUtils.leftPad("bat", 3)  = "bat"
+     * StringUtils.leftPad("bat", 5)  = "  bat"
+     * StringUtils.leftPad("bat", 1)  = "bat"
+     * StringUtils.leftPad("bat", -1) = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String leftPad(final String str, final int size) { + return leftPad(str, size, ' '); + } + + /** + *

Left pad a String with a specified character.

+ *

+ *

Pad to a size of {@code size}.

+ *

+ *

+     * StringUtils.leftPad(null, *, *)     = null
+     * StringUtils.leftPad("", 3, 'z')     = "zzz"
+     * StringUtils.leftPad("bat", 3, 'z')  = "bat"
+     * StringUtils.leftPad("bat", 5, 'z')  = "zzbat"
+     * StringUtils.leftPad("bat", 1, 'z')  = "bat"
+     * StringUtils.leftPad("bat", -1, 'z') = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padChar the character to pad with + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + * @since 2.0 + */ + public static String leftPad(final String str, final int size, final char padChar) { + if (str == null) { + return null; + } + final int pads = size - str.length(); + if (pads <= 0) { + return str; // returns original String when possible + } + if (pads > PAD_LIMIT) { + return leftPad(str, size, String.valueOf(padChar)); + } + return repeat(padChar, pads).concat(str); + } + + /** + *

Left pad a String with a specified String.

+ *

+ *

Pad to a size of {@code size}.

+ *

+ *

+     * StringUtils.leftPad(null, *, *)      = null
+     * StringUtils.leftPad("", 3, "z")      = "zzz"
+     * StringUtils.leftPad("bat", 3, "yz")  = "bat"
+     * StringUtils.leftPad("bat", 5, "yz")  = "yzbat"
+     * StringUtils.leftPad("bat", 8, "yz")  = "yzyzybat"
+     * StringUtils.leftPad("bat", 1, "yz")  = "bat"
+     * StringUtils.leftPad("bat", -1, "yz") = "bat"
+     * StringUtils.leftPad("bat", 5, null)  = "  bat"
+     * StringUtils.leftPad("bat", 5, "")    = "  bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padStr the String to pad with, null or empty treated as single space + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String leftPad(final String str, final int size, String padStr) { + if (str == null) { + return null; + } + if (isEmpty(padStr)) { + padStr = SPACE; + } + final int padLen = padStr.length(); + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str; // returns original String when possible + } + if (padLen == 1 && pads <= PAD_LIMIT) { + return leftPad(str, size, padStr.charAt(0)); + } + + if (pads == padLen) { + return padStr.concat(str); + } else if (pads < padLen) { + return padStr.substring(0, pads).concat(str); + } else { + final char[] padding = new char[pads]; + final char[] padChars = padStr.toCharArray(); + for (int i = 0; i < pads; i++) { + padding[i] = padChars[i % padLen]; + } + return new String(padding).concat(str); + } + } + + /** + * Gets a CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * + * @param cs a CharSequence or {@code null} + * @return CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * @since 3.0 Changed signature from length(String) to length(CharSequence) + */ + public static int length(final CharSequence cs) { + return cs == null ? 0 : cs.length(); + } + + // Centering + //----------------------------------------------------------------------- + + /** + *

Centers a String in a larger String of size {@code size} + * using the space character (' ').

+ *

+ *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ *

+ *

Equivalent to {@code center(str, size, " ")}.

+ *

+ *

+     * StringUtils.center(null, *)   = null
+     * StringUtils.center("", 4)     = "    "
+     * StringUtils.center("ab", -1)  = "ab"
+     * StringUtils.center("ab", 4)   = " ab "
+     * StringUtils.center("abcd", 2) = "abcd"
+     * StringUtils.center("a", 4)    = " a  "
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @return centered String, {@code null} if null String input + */ + public static String center(final String str, final int size) { + return center(str, size, ' '); + } + + /** + *

Centers a String in a larger String of size {@code size}. + * Uses a supplied character as the value to pad the String with.

+ *

+ *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ *

+ *

+     * StringUtils.center(null, *, *)     = null
+     * StringUtils.center("", 4, ' ')     = "    "
+     * StringUtils.center("ab", -1, ' ')  = "ab"
+     * StringUtils.center("ab", 4, ' ')   = " ab "
+     * StringUtils.center("abcd", 2, ' ') = "abcd"
+     * StringUtils.center("a", 4, ' ')    = " a  "
+     * StringUtils.center("a", 4, 'y')    = "yayy"
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @param padChar the character to pad the new String with + * @return centered String, {@code null} if null String input + * @since 2.0 + */ + public static String center(String str, final int size, final char padChar) { + if (str == null || size <= 0) { + return str; + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str; + } + str = leftPad(str, strLen + pads / 2, padChar); + str = rightPad(str, size, padChar); + return str; + } + + /** + *

Centers a String in a larger String of size {@code size}. + * Uses a supplied String as the value to pad the String with.

+ *

+ *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ *

+ *

+     * StringUtils.center(null, *, *)     = null
+     * StringUtils.center("", 4, " ")     = "    "
+     * StringUtils.center("ab", -1, " ")  = "ab"
+     * StringUtils.center("ab", 4, " ")   = " ab "
+     * StringUtils.center("abcd", 2, " ") = "abcd"
+     * StringUtils.center("a", 4, " ")    = " a  "
+     * StringUtils.center("a", 4, "yz")   = "yayz"
+     * StringUtils.center("abc", 7, null) = "  abc  "
+     * StringUtils.center("abc", 7, "")   = "  abc  "
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @param padStr the String to pad the new String with, must not be null or empty + * @return centered String, {@code null} if null String input + * @throws IllegalArgumentException if padStr is {@code null} or empty + */ + public static String center(String str, final int size, String padStr) { + if (str == null || size <= 0) { + return str; + } + if (isEmpty(padStr)) { + padStr = SPACE; + } + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str; + } + str = leftPad(str, strLen + pads / 2, padStr); + str = rightPad(str, size, padStr); + return str; + } + + // Case conversion + //----------------------------------------------------------------------- + + /** + *

Converts a String to upper case as per {@link String#toUpperCase()}.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.upperCase(null)  = null
+     * StringUtils.upperCase("")    = ""
+     * StringUtils.upperCase("aBc") = "ABC"
+     * 
+ *

+ *

Note: As described in the documentation for {@link String#toUpperCase()}, + * the result of this method is affected by the current locale. + * For platform-independent case transformations, the method {@link #lowerCase(String, Locale)} + * should be used with a specific locale (e.g. {@link Locale#ENGLISH}).

+ * + * @param str the String to upper case, may be null + * @return the upper cased String, {@code null} if null String input + */ + public static String upperCase(final String str) { + if (str == null) { + return null; + } + return str.toUpperCase(); + } + + /** + *

Converts a String to upper case as per {@link String#toUpperCase(Locale)}.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.upperCase(null, Locale.ENGLISH)  = null
+     * StringUtils.upperCase("", Locale.ENGLISH)    = ""
+     * StringUtils.upperCase("aBc", Locale.ENGLISH) = "ABC"
+     * 
+ * + * @param str the String to upper case, may be null + * @param locale the locale that defines the case transformation rules, must not be null + * @return the upper cased String, {@code null} if null String input + * @since 2.5 + */ + public static String upperCase(final String str, final Locale locale) { + if (str == null) { + return null; + } + return str.toUpperCase(locale); + } + + /** + *

Converts a String to lower case as per {@link String#toLowerCase()}.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.lowerCase(null)  = null
+     * StringUtils.lowerCase("")    = ""
+     * StringUtils.lowerCase("aBc") = "abc"
+     * 
+ *

+ *

Note: As described in the documentation for {@link String#toLowerCase()}, + * the result of this method is affected by the current locale. + * For platform-independent case transformations, the method {@link #lowerCase(String, Locale)} + * should be used with a specific locale (e.g. {@link Locale#ENGLISH}).

+ * + * @param str the String to lower case, may be null + * @return the lower cased String, {@code null} if null String input + */ + public static String lowerCase(final String str) { + if (str == null) { + return null; + } + return str.toLowerCase(); + } + + /** + *

Converts a String to lower case as per {@link String#toLowerCase(Locale)}.

+ *

+ *

A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.lowerCase(null, Locale.ENGLISH)  = null
+     * StringUtils.lowerCase("", Locale.ENGLISH)    = ""
+     * StringUtils.lowerCase("aBc", Locale.ENGLISH) = "abc"
+     * 
+ * + * @param str the String to lower case, may be null + * @param locale the locale that defines the case transformation rules, must not be null + * @return the lower cased String, {@code null} if null String input + * @since 2.5 + */ + public static String lowerCase(final String str, final Locale locale) { + if (str == null) { + return null; + } + return str.toLowerCase(locale); + } + + /** + *

Capitalizes a String changing the first letter to title case as + * per {@link Character#toTitleCase(char)}. No other letters are changed.

+ *

+ *

For a word based algorithm, see {@link org.apache.commons.lang3.text.WordUtils#capitalize(String)}. + * A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.capitalize(null)  = null
+     * StringUtils.capitalize("")    = ""
+     * StringUtils.capitalize("cat") = "Cat"
+     * StringUtils.capitalize("cAt") = "CAt"
+     * 
+ * + * @param str the String to capitalize, may be null + * @return the capitalized String, {@code null} if null String input + * @see org.apache.commons.lang3.text.WordUtils#capitalize(String) + * @see #uncapitalize(String) + * @since 2.0 + */ + public static String capitalize(final String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + + char firstChar = str.charAt(0); + if (Character.isTitleCase(firstChar)) { + // already capitalized + return str; + } + + return new StringBuilder(strLen) + .append(Character.toTitleCase(firstChar)) + .append(str.substring(1)) + .toString(); + } + + /** + *

Uncapitalizes a String changing the first letter to title case as + * per {@link Character#toLowerCase(char)}. No other letters are changed.

+ *

+ *

For a word based algorithm, see {@link org.apache.commons.lang3.text.WordUtils#uncapitalize(String)}. + * A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.uncapitalize(null)  = null
+     * StringUtils.uncapitalize("")    = ""
+     * StringUtils.uncapitalize("Cat") = "cat"
+     * StringUtils.uncapitalize("CAT") = "cAT"
+     * 
+ * + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, {@code null} if null String input + * @see #capitalize(String) + * @since 2.0 + */ + public static String uncapitalize(final String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + + char firstChar = str.charAt(0); + if (Character.isLowerCase(firstChar)) { + // already uncapitalized + return str; + } + + return new StringBuilder(strLen) + .append(Character.toLowerCase(firstChar)) + .append(str.substring(1)) + .toString(); + } + + /** + *

Swaps the case of a String changing upper and title case to + * lower case, and lower case to upper case.

+ *

+ *

    + *
  • Upper case character converts to Lower case
  • + *
  • Title case character converts to Lower case
  • + *
  • Lower case character converts to Upper case
  • + *
+ *

+ *

For a word based algorithm, see {@link org.apache.commons.lang3.text.WordUtils#swapCase(String)}. + * A {@code null} input String returns {@code null}.

+ *

+ *

+     * StringUtils.swapCase(null)                 = null
+     * StringUtils.swapCase("")                   = ""
+     * StringUtils.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
+     * 
+ *

+ *

NOTE: This method changed in Lang version 2.0. + * It no longer performs a word based algorithm. + * If you only use ASCII, you will notice no change. + * That functionality is available in org.apache.commons.lang3.text.WordUtils.

+ * + * @param str the String to swap case, may be null + * @return the changed String, {@code null} if null String input + */ + public static String swapCase(final String str) { + if (StringUtils.isEmpty(str)) { + return str; + } + + final char[] buffer = str.toCharArray(); + + for (int i = 0; i < buffer.length; i++) { + final char ch = buffer[i]; + if (Character.isUpperCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isTitleCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isLowerCase(ch)) { + buffer[i] = Character.toUpperCase(ch); + } + } + return new String(buffer); + } + + // Count matches + //----------------------------------------------------------------------- + + /** + *

Counts how many times the substring appears in the larger string.

+ *

+ *

A {@code null} or empty ("") String input returns {@code 0}.

+ *

+ *

+     * StringUtils.countMatches(null, *)       = 0
+     * StringUtils.countMatches("", *)         = 0
+     * StringUtils.countMatches("abba", null)  = 0
+     * StringUtils.countMatches("abba", "")    = 0
+     * StringUtils.countMatches("abba", "a")   = 2
+     * StringUtils.countMatches("abba", "ab")  = 1
+     * StringUtils.countMatches("abba", "xxx") = 0
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param sub the substring to count, may be null + * @return the number of occurrences, 0 if either CharSequence is {@code null} + * @since 3.0 Changed signature from countMatches(String, String) to countMatches(CharSequence, CharSequence) + */ + public static int countMatches(final CharSequence str, final CharSequence sub) { + if (isEmpty(str) || isEmpty(sub)) { + return 0; + } + int count = 0; + int idx = 0; + while ((idx = CharSequenceUtils.indexOf(str, sub, idx)) != INDEX_NOT_FOUND) { + count++; + idx += sub.length(); + } + return count; + } + + // Character Tests + //----------------------------------------------------------------------- + + /** + *

Checks if the CharSequence contains only Unicode letters.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ *

+ *

+     * StringUtils.isAlpha(null)   = false
+     * StringUtils.isAlpha("")     = false
+     * StringUtils.isAlpha("  ")   = false
+     * StringUtils.isAlpha("abc")  = true
+     * StringUtils.isAlpha("ab2c") = false
+     * StringUtils.isAlpha("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters, and is non-null + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isAlpha(final CharSequence cs) { + if (isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetter(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters and + * space (' ').

+ *

+ *

{@code null} will return {@code false} + * An empty CharSequence (length()=0) will return {@code true}.

+ *

+ *

+     * StringUtils.isAlphaSpace(null)   = false
+     * StringUtils.isAlphaSpace("")     = true
+     * StringUtils.isAlphaSpace("  ")   = true
+     * StringUtils.isAlphaSpace("abc")  = true
+     * StringUtils.isAlphaSpace("ab c") = true
+     * StringUtils.isAlphaSpace("ab2c") = false
+     * StringUtils.isAlphaSpace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters and space, + * and is non-null + * @since 3.0 Changed signature from isAlphaSpace(String) to isAlphaSpace(CharSequence) + */ + public static boolean isAlphaSpace(final CharSequence cs) { + if (cs == null) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetter(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters or digits.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ *

+ *

+     * StringUtils.isAlphanumeric(null)   = false
+     * StringUtils.isAlphanumeric("")     = false
+     * StringUtils.isAlphanumeric("  ")   = false
+     * StringUtils.isAlphanumeric("abc")  = true
+     * StringUtils.isAlphanumeric("ab c") = false
+     * StringUtils.isAlphanumeric("ab2c") = true
+     * StringUtils.isAlphanumeric("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters or digits, + * and is non-null + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isAlphanumeric(final CharSequence cs) { + if (isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetterOrDigit(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters, digits + * or space ({@code ' '}).

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ *

+ *

+     * StringUtils.isAlphanumericSpace(null)   = false
+     * StringUtils.isAlphanumericSpace("")     = true
+     * StringUtils.isAlphanumericSpace("  ")   = true
+     * StringUtils.isAlphanumericSpace("abc")  = true
+     * StringUtils.isAlphanumericSpace("ab c") = true
+     * StringUtils.isAlphanumericSpace("ab2c") = true
+     * StringUtils.isAlphanumericSpace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters, digits or space, + * and is non-null + * @since 3.0 Changed signature from isAlphanumericSpace(String) to isAlphanumericSpace(CharSequence) + */ + public static boolean isAlphanumericSpace(final CharSequence cs) { + if (cs == null) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetterOrDigit(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only ASCII printable characters.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ *

+ *

+     * StringUtils.isAsciiPrintable(null)     = false
+     * StringUtils.isAsciiPrintable("")       = true
+     * StringUtils.isAsciiPrintable(" ")      = true
+     * StringUtils.isAsciiPrintable("Ceki")   = true
+     * StringUtils.isAsciiPrintable("ab2c")   = true
+     * StringUtils.isAsciiPrintable("!ab-c~") = true
+     * StringUtils.isAsciiPrintable("\u0020") = true
+     * StringUtils.isAsciiPrintable("\u0021") = true
+     * StringUtils.isAsciiPrintable("\u007e") = true
+     * StringUtils.isAsciiPrintable("\u007f") = false
+     * StringUtils.isAsciiPrintable("Ceki G\u00fclc\u00fc") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if every character is in the range + * 32 thru 126 + * @since 3.0 Changed signature from isAsciiPrintable(String) to isAsciiPrintable(CharSequence) + */ + public static boolean isAsciiPrintable(final CharSequence cs) { + if (cs == null) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (CharUtils.isAsciiPrintable(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode digits. + * A decimal point is not a Unicode digit and returns false.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ *

+ *

Note that the method does not allow for a leading sign, either positive or negative. + * Also, if a String passes the numeric test, it may still generate a NumberFormatException + * when parsed by Integer.parseInt or Long.parseLong, e.g. if the value is outside the range + * for int or long respectively.

+ *

+ *

+     * StringUtils.isNumeric(null)   = false
+     * StringUtils.isNumeric("")     = false
+     * StringUtils.isNumeric("  ")   = false
+     * StringUtils.isNumeric("123")  = true
+     * StringUtils.isNumeric("12 3") = false
+     * StringUtils.isNumeric("ab2c") = false
+     * StringUtils.isNumeric("12-3") = false
+     * StringUtils.isNumeric("12.3") = false
+     * StringUtils.isNumeric("-123") = false
+     * StringUtils.isNumeric("+123") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains digits, and is non-null + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isNumeric(final CharSequence cs) { + if (isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isDigit(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode digits or space + * ({@code ' '}). + * A decimal point is not a Unicode digit and returns false.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ *

+ *

+     * StringUtils.isNumericSpace(null)   = false
+     * StringUtils.isNumericSpace("")     = true
+     * StringUtils.isNumericSpace("  ")   = true
+     * StringUtils.isNumericSpace("123")  = true
+     * StringUtils.isNumericSpace("12 3") = true
+     * StringUtils.isNumericSpace("ab2c") = false
+     * StringUtils.isNumericSpace("12-3") = false
+     * StringUtils.isNumericSpace("12.3") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains digits or space, + * and is non-null + * @since 3.0 Changed signature from isNumericSpace(String) to isNumericSpace(CharSequence) + */ + public static boolean isNumericSpace(final CharSequence cs) { + if (cs == null) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isDigit(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only whitespace.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ *

+ *

+     * StringUtils.isWhitespace(null)   = false
+     * StringUtils.isWhitespace("")     = true
+     * StringUtils.isWhitespace("  ")   = true
+     * StringUtils.isWhitespace("abc")  = false
+     * StringUtils.isWhitespace("ab2c") = false
+     * StringUtils.isWhitespace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains whitespace, and is non-null + * @since 3.0 Changed signature from isWhitespace(String) to isWhitespace(CharSequence) + */ + public static boolean isWhitespace(final CharSequence cs) { + if (cs == null) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isWhitespace(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only lowercase characters.

+ *

+ *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ *

+ *

+     * StringUtils.isAllLowerCase(null)   = false
+     * StringUtils.isAllLowerCase("")     = false
+     * StringUtils.isAllLowerCase("  ")   = false
+     * StringUtils.isAllLowerCase("abc")  = true
+     * StringUtils.isAllLowerCase("abC") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains lowercase characters, and is non-null + * @since 3.0 Changed signature from isAllLowerCase(String) to isAllLowerCase(CharSequence) + */ + public static boolean isAllLowerCase(final CharSequence cs) { + if (cs == null || isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLowerCase(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only uppercase characters.

+ *

+ *

{@code null} will return {@code false}. + * An empty String (length()=0) will return {@code false}.

+ *

+ *

+     * StringUtils.isAllUpperCase(null)   = false
+     * StringUtils.isAllUpperCase("")     = false
+     * StringUtils.isAllUpperCase("  ")   = false
+     * StringUtils.isAllUpperCase("ABC")  = true
+     * StringUtils.isAllUpperCase("aBC") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains uppercase characters, and is non-null + * @since 3.0 Changed signature from isAllUpperCase(String) to isAllUpperCase(CharSequence) + */ + public static boolean isAllUpperCase(final CharSequence cs) { + if (cs == null || isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isUpperCase(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + // Defaults + //----------------------------------------------------------------------- + + /** + *

Returns either the passed in String, + * or if the String is {@code null}, an empty String ("").

+ *

+ *

+     * StringUtils.defaultString(null)  = ""
+     * StringUtils.defaultString("")    = ""
+     * StringUtils.defaultString("bat") = "bat"
+     * 
+ * + * @param str the String to check, may be null + * @return the passed in String, or the empty String if it + * was {@code null} + * @see ObjectUtils#toString(Object) + * @see String#valueOf(Object) + */ + public static String defaultString(final String str) { + return str == null ? EMPTY : str; + } + + /** + *

Returns either the passed in String, or if the String is + * {@code null}, the value of {@code defaultStr}.

+ *

+ *

+     * StringUtils.defaultString(null, "NULL")  = "NULL"
+     * StringUtils.defaultString("", "NULL")    = ""
+     * StringUtils.defaultString("bat", "NULL") = "bat"
+     * 
+ * + * @param str the String to check, may be null + * @param defaultStr the default String to return + * if the input is {@code null}, may be null + * @return the passed in String, or the default if it was {@code null} + * @see ObjectUtils#toString(Object, String) + * @see String#valueOf(Object) + */ + public static String defaultString(final String str, final String defaultStr) { + return str == null ? defaultStr : str; + } + + /** + *

Returns either the passed in CharSequence, or if the CharSequence is + * whitespace, empty ("") or {@code null}, the value of {@code defaultStr}.

+ *

+ *

+     * StringUtils.defaultIfBlank(null, "NULL")  = "NULL"
+     * StringUtils.defaultIfBlank("", "NULL")    = "NULL"
+     * StringUtils.defaultIfBlank(" ", "NULL")   = "NULL"
+     * StringUtils.defaultIfBlank("bat", "NULL") = "bat"
+     * StringUtils.defaultIfBlank("", null)      = null
+     * 
+ * + * @param the specific kind of CharSequence + * @param str the CharSequence to check, may be null + * @param defaultStr the default CharSequence to return + * if the input is whitespace, empty ("") or {@code null}, may be null + * @return the passed in CharSequence, or the default + * @see StringUtils#defaultString(String, String) + */ + public static T defaultIfBlank(final T str, final T defaultStr) { + return StringUtils.isBlank(str) ? defaultStr : str; + } + + /** + *

Returns either the passed in CharSequence, or if the CharSequence is + * empty or {@code null}, the value of {@code defaultStr}.

+ *

+ *

+     * StringUtils.defaultIfEmpty(null, "NULL")  = "NULL"
+     * StringUtils.defaultIfEmpty("", "NULL")    = "NULL"
+     * StringUtils.defaultIfEmpty(" ", "NULL")   = " "
+     * StringUtils.defaultIfEmpty("bat", "NULL") = "bat"
+     * StringUtils.defaultIfEmpty("", null)      = null
+     * 
+ * + * @param the specific kind of CharSequence + * @param str the CharSequence to check, may be null + * @param defaultStr the default CharSequence to return + * if the input is empty ("") or {@code null}, may be null + * @return the passed in CharSequence, or the default + * @see StringUtils#defaultString(String, String) + */ + public static T defaultIfEmpty(final T str, final T defaultStr) { + return StringUtils.isEmpty(str) ? defaultStr : str; + } + + // Reversing + //----------------------------------------------------------------------- + + /** + *

Reverses a String as per {@link StringBuilder#reverse()}.

+ *

+ *

A {@code null} String returns {@code null}.

+ *

+ *

+     * StringUtils.reverse(null)  = null
+     * StringUtils.reverse("")    = ""
+     * StringUtils.reverse("bat") = "tab"
+     * 
+ * + * @param str the String to reverse, may be null + * @return the reversed String, {@code null} if null String input + */ + public static String reverse(final String str) { + if (str == null) { + return null; + } + return new StringBuilder(str).reverse().toString(); + } + + /** + *

Reverses a String that is delimited by a specific character.

+ *

+ *

The Strings between the delimiters are not reversed. + * Thus java.lang.String becomes String.lang.java (if the delimiter + * is {@code '.'}).

+ *

+ *

+     * StringUtils.reverseDelimited(null, *)      = null
+     * StringUtils.reverseDelimited("", *)        = ""
+     * StringUtils.reverseDelimited("a.b.c", 'x') = "a.b.c"
+     * StringUtils.reverseDelimited("a.b.c", ".") = "c.b.a"
+     * 
+ * + * @param str the String to reverse, may be null + * @param separatorChar the separator character to use + * @return the reversed String, {@code null} if null String input + * @since 2.0 + */ + public static String reverseDelimited(final String str, final char separatorChar) { + if (str == null) { + return null; + } + // could implement manually, but simple way is to reuse other, + // probably slower, methods. + final String[] strs = split(str, separatorChar); + ArrayUtils.reverse(strs); + return join(strs, separatorChar); + } + + // Abbreviating + //----------------------------------------------------------------------- + + /** + *

Abbreviates a String using ellipses. This will turn + * "Now is the time for all good men" into "Now is the time for..."

+ *

+ *

Specifically:

+ *
    + *
  • If {@code str} is less than {@code maxWidth} characters + * long, return it.
  • + *
  • Else abbreviate it to {@code (substring(str, 0, max-3) + "...")}.
  • + *
  • If {@code maxWidth} is less than {@code 4}, throw an + * {@code IllegalArgumentException}.
  • + *
  • In no case will it return a String of length greater than + * {@code maxWidth}.
  • + *
+ *

+ *

+     * StringUtils.abbreviate(null, *)      = null
+     * StringUtils.abbreviate("", 4)        = ""
+     * StringUtils.abbreviate("abcdefg", 6) = "abc..."
+     * StringUtils.abbreviate("abcdefg", 7) = "abcdefg"
+     * StringUtils.abbreviate("abcdefg", 8) = "abcdefg"
+     * StringUtils.abbreviate("abcdefg", 4) = "a..."
+     * StringUtils.abbreviate("abcdefg", 3) = IllegalArgumentException
+     * 
+ * + * @param str the String to check, may be null + * @param maxWidth maximum length of result String, must be at least 4 + * @return abbreviated String, {@code null} if null String input + * @throws IllegalArgumentException if the width is too small + * @since 2.0 + */ + public static String abbreviate(final String str, final int maxWidth) { + return abbreviate(str, 0, maxWidth); + } + + /** + *

Abbreviates a String using ellipses. This will turn + * "Now is the time for all good men" into "...is the time for..."

+ *

+ *

Works like {@code abbreviate(String, int)}, but allows you to specify + * a "left edge" offset. Note that this left edge is not necessarily going to + * be the leftmost character in the result, or the first character following the + * ellipses, but it will appear somewhere in the result. + *

+ *

In no case will it return a String of length greater than + * {@code maxWidth}.

+ *

+ *

+     * StringUtils.abbreviate(null, *, *)                = null
+     * StringUtils.abbreviate("", 0, 4)                  = ""
+     * StringUtils.abbreviate("abcdefghijklmno", -1, 10) = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 0, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 1, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 4, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 5, 10)  = "...fghi..."
+     * StringUtils.abbreviate("abcdefghijklmno", 6, 10)  = "...ghij..."
+     * StringUtils.abbreviate("abcdefghijklmno", 8, 10)  = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghijklmno", 10, 10) = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghijklmno", 12, 10) = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghij", 0, 3)        = IllegalArgumentException
+     * StringUtils.abbreviate("abcdefghij", 5, 6)        = IllegalArgumentException
+     * 
+ * + * @param str the String to check, may be null + * @param offset left edge of source String + * @param maxWidth maximum length of result String, must be at least 4 + * @return abbreviated String, {@code null} if null String input + * @throws IllegalArgumentException if the width is too small + * @since 2.0 + */ + public static String abbreviate(final String str, int offset, final int maxWidth) { + if (str == null) { + return null; + } + if (maxWidth < 4) { + throw new IllegalArgumentException("Minimum abbreviation width is 4"); + } + if (str.length() <= maxWidth) { + return str; + } + if (offset > str.length()) { + offset = str.length(); + } + if (str.length() - offset < maxWidth - 3) { + offset = str.length() - (maxWidth - 3); + } + final String abrevMarker = "..."; + if (offset <= 4) { + return str.substring(0, maxWidth - 3) + abrevMarker; + } + if (maxWidth < 7) { + throw new IllegalArgumentException("Minimum abbreviation width with offset is 7"); + } + if (offset + maxWidth - 3 < str.length()) { + return abrevMarker + abbreviate(str.substring(offset), maxWidth - 3); + } + return abrevMarker + str.substring(str.length() - (maxWidth - 3)); + } + + /** + *

Abbreviates a String to the length passed, replacing the middle characters with the supplied + * replacement String.

+ *

+ *

This abbreviation only occurs if the following criteria is met:

+ *
    + *
  • Neither the String for abbreviation nor the replacement String are null or empty
  • + *
  • The length to truncate to is less than the length of the supplied String
  • + *
  • The length to truncate to is greater than 0
  • + *
  • The abbreviated String will have enough room for the length supplied replacement String + * and the first and last characters of the supplied String for abbreviation
  • + *
+ *

Otherwise, the returned String will be the same as the supplied String for abbreviation. + *

+ *

+ *

+     * StringUtils.abbreviateMiddle(null, null, 0)      = null
+     * StringUtils.abbreviateMiddle("abc", null, 0)      = "abc"
+     * StringUtils.abbreviateMiddle("abc", ".", 0)      = "abc"
+     * StringUtils.abbreviateMiddle("abc", ".", 3)      = "abc"
+     * StringUtils.abbreviateMiddle("abcdef", ".", 4)     = "ab.f"
+     * 
+ * + * @param str the String to abbreviate, may be null + * @param middle the String to replace the middle characters with, may be null + * @param length the length to abbreviate {@code str} to. + * @return the abbreviated String if the above criteria is met, or the original String supplied for abbreviation. + * @since 2.5 + */ + public static String abbreviateMiddle(final String str, final String middle, final int length) { + if (isEmpty(str) || isEmpty(middle)) { + return str; + } + + if (length >= str.length() || length < middle.length() + 2) { + return str; + } + + final int targetSting = length - middle.length(); + final int startOffset = targetSting / 2 + targetSting % 2; + final int endOffset = str.length() - targetSting / 2; + + final StringBuilder builder = new StringBuilder(length); + builder.append(str.substring(0, startOffset)); + builder.append(middle); + builder.append(str.substring(endOffset)); + + return builder.toString(); + } + + // Difference + //----------------------------------------------------------------------- + + /** + *

Compares two Strings, and returns the portion where they differ. + * More precisely, return the remainder of the second String, + * starting from where it's different from the first. This means that + * the difference between "abc" and "ab" is the empty String and not "c".

+ *

+ *

For example, + * {@code difference("i am a machine", "i am a robot") -> "robot"}.

+ *

+ *

+     * StringUtils.difference(null, null) = null
+     * StringUtils.difference("", "") = ""
+     * StringUtils.difference("", "abc") = "abc"
+     * StringUtils.difference("abc", "") = ""
+     * StringUtils.difference("abc", "abc") = ""
+     * StringUtils.difference("abc", "ab") = ""
+     * StringUtils.difference("ab", "abxyz") = "xyz"
+     * StringUtils.difference("abcde", "abxyz") = "xyz"
+     * StringUtils.difference("abcde", "xyz") = "xyz"
+     * 
+ * + * @param str1 the first String, may be null + * @param str2 the second String, may be null + * @return the portion of str2 where it differs from str1; returns the + * empty String if they are equal + * @see #indexOfDifference(CharSequence, CharSequence) + * @since 2.0 + */ + public static String difference(final String str1, final String str2) { + if (str1 == null) { + return str2; + } + if (str2 == null) { + return str1; + } + final int at = indexOfDifference(str1, str2); + if (at == INDEX_NOT_FOUND) { + return EMPTY; + } + return str2.substring(at); + } + + /** + *

Compares two CharSequences, and returns the index at which the + * CharSequences begin to differ.

+ *

+ *

For example, + * {@code indexOfDifference("i am a machine", "i am a robot") -> 7}

+ *

+ *

+     * StringUtils.indexOfDifference(null, null) = -1
+     * StringUtils.indexOfDifference("", "") = -1
+     * StringUtils.indexOfDifference("", "abc") = 0
+     * StringUtils.indexOfDifference("abc", "") = 0
+     * StringUtils.indexOfDifference("abc", "abc") = -1
+     * StringUtils.indexOfDifference("ab", "abxyz") = 2
+     * StringUtils.indexOfDifference("abcde", "abxyz") = 2
+     * StringUtils.indexOfDifference("abcde", "xyz") = 0
+     * 
+ * + * @param cs1 the first CharSequence, may be null + * @param cs2 the second CharSequence, may be null + * @return the index where cs1 and cs2 begin to differ; -1 if they are equal + * @since 3.0 Changed signature from indexOfDifference(String, String) to + * indexOfDifference(CharSequence, CharSequence) + */ + public static int indexOfDifference(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return INDEX_NOT_FOUND; + } + if (cs1 == null || cs2 == null) { + return 0; + } + int i; + for (i = 0; i < cs1.length() && i < cs2.length(); ++i) { + if (cs1.charAt(i) != cs2.charAt(i)) { + break; + } + } + if (i < cs2.length() || i < cs1.length()) { + return i; + } + return INDEX_NOT_FOUND; + } + + /** + *

Compares all CharSequences in an array and returns the index at which the + * CharSequences begin to differ.

+ *

+ *

For example, + * indexOfDifference(new String[] {"i am a machine", "i am a robot"}) -> 7

+ *

+ *

+     * StringUtils.indexOfDifference(null) = -1
+     * StringUtils.indexOfDifference(new String[] {}) = -1
+     * StringUtils.indexOfDifference(new String[] {"abc"}) = -1
+     * StringUtils.indexOfDifference(new String[] {null, null}) = -1
+     * StringUtils.indexOfDifference(new String[] {"", ""}) = -1
+     * StringUtils.indexOfDifference(new String[] {"", null}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", null, null}) = 0
+     * StringUtils.indexOfDifference(new String[] {null, null, "abc"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"", "abc"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", ""}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", "abc"}) = -1
+     * StringUtils.indexOfDifference(new String[] {"abc", "a"}) = 1
+     * StringUtils.indexOfDifference(new String[] {"ab", "abxyz"}) = 2
+     * StringUtils.indexOfDifference(new String[] {"abcde", "abxyz"}) = 2
+     * StringUtils.indexOfDifference(new String[] {"abcde", "xyz"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"xyz", "abcde"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"i am a machine", "i am a robot"}) = 7
+     * 
+ * + * @param css array of CharSequences, entries may be null + * @return the index where the strings begin to differ; -1 if they are all equal + * @since 3.0 Changed signature from indexOfDifference(String...) to indexOfDifference(CharSequence...) + */ + public static int indexOfDifference(final CharSequence... css) { + if (css == null || css.length <= 1) { + return INDEX_NOT_FOUND; + } + boolean anyStringNull = false; + boolean allStringsNull = true; + final int arrayLen = css.length; + int shortestStrLen = Integer.MAX_VALUE; + int longestStrLen = 0; + + // find the min and max string lengths; this avoids checking to make + // sure we are not exceeding the length of the string each time through + // the bottom loop. + for (int i = 0; i < arrayLen; i++) { + if (css[i] == null) { + anyStringNull = true; + shortestStrLen = 0; + } else { + allStringsNull = false; + shortestStrLen = Math.min(css[i].length(), shortestStrLen); + longestStrLen = Math.max(css[i].length(), longestStrLen); + } + } + + // handle lists containing all nulls or all empty strings + if (allStringsNull || longestStrLen == 0 && !anyStringNull) { + return INDEX_NOT_FOUND; + } + + // handle lists containing some nulls or some empty strings + if (shortestStrLen == 0) { + return 0; + } + + // find the position with the first difference across all strings + int firstDiff = -1; + for (int stringPos = 0; stringPos < shortestStrLen; stringPos++) { + final char comparisonChar = css[0].charAt(stringPos); + for (int arrayPos = 1; arrayPos < arrayLen; arrayPos++) { + if (css[arrayPos].charAt(stringPos) != comparisonChar) { + firstDiff = stringPos; + break; + } + } + if (firstDiff != -1) { + break; + } + } + + if (firstDiff == -1 && shortestStrLen != longestStrLen) { + // we compared all of the characters up to the length of the + // shortest string and didn't find a match, but the string lengths + // vary, so return the length of the shortest string. + return shortestStrLen; + } + return firstDiff; + } + + /** + *

Compares all Strings in an array and returns the initial sequence of + * characters that is common to all of them.

+ *

+ *

For example, + * getCommonPrefix(new String[] {"i am a machine", "i am a robot"}) -> "i am a "

+ *

+ *

+     * StringUtils.getCommonPrefix(null) = ""
+     * StringUtils.getCommonPrefix(new String[] {}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc"}) = "abc"
+     * StringUtils.getCommonPrefix(new String[] {null, null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", ""}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", null, null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {null, null, "abc"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", "abc"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", ""}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", "abc"}) = "abc"
+     * StringUtils.getCommonPrefix(new String[] {"abc", "a"}) = "a"
+     * StringUtils.getCommonPrefix(new String[] {"ab", "abxyz"}) = "ab"
+     * StringUtils.getCommonPrefix(new String[] {"abcde", "abxyz"}) = "ab"
+     * StringUtils.getCommonPrefix(new String[] {"abcde", "xyz"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"xyz", "abcde"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"i am a machine", "i am a robot"}) = "i am a "
+     * 
+ * + * @param strs array of String objects, entries may be null + * @return the initial sequence of characters that are common to all Strings + * in the array; empty String if the array is null, the elements are all null + * or if there is no common prefix. + * @since 2.4 + */ + public static String getCommonPrefix(final String... strs) { + if (strs == null || strs.length == 0) { + return EMPTY; + } + final int smallestIndexOfDiff = indexOfDifference(strs); + if (smallestIndexOfDiff == INDEX_NOT_FOUND) { + // all strings were identical + if (strs[0] == null) { + return EMPTY; + } + return strs[0]; + } else if (smallestIndexOfDiff == 0) { + // there were no common initial characters + return EMPTY; + } else { + // we found a common initial character sequence + return strs[0].substring(0, smallestIndexOfDiff); + } + } + + // Misc + //----------------------------------------------------------------------- + + /** + *

Find the Levenshtein distance between two Strings.

+ *

+ *

This is the number of changes needed to change one String into + * another, where each change is a single character modification (deletion, + * insertion or substitution).

+ *

+ *

The previous implementation of the Levenshtein distance algorithm + * was from http://www.merriampark.com/ld.htm

+ *

+ *

Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError + * which can occur when my Java implementation is used with very large strings.
+ * This implementation of the Levenshtein distance algorithm + * is from http://www.merriampark.com/ldjava.htm

+ *

+ *

+     * StringUtils.getLevenshteinDistance(null, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, null)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance("","")               = 0
+     * StringUtils.getLevenshteinDistance("","a")              = 1
+     * StringUtils.getLevenshteinDistance("aaapppp", "")       = 7
+     * StringUtils.getLevenshteinDistance("frog", "fog")       = 1
+     * StringUtils.getLevenshteinDistance("fly", "ant")        = 3
+     * StringUtils.getLevenshteinDistance("elephant", "hippo") = 7
+     * StringUtils.getLevenshteinDistance("hippo", "elephant") = 7
+     * StringUtils.getLevenshteinDistance("hippo", "zzzzzzzz") = 8
+     * StringUtils.getLevenshteinDistance("hello", "hallo")    = 1
+     * 
+ * + * @param s the first String, must not be null + * @param t the second String, must not be null + * @return result distance + * @throws IllegalArgumentException if either String input {@code null} + * @since 3.0 Changed signature from getLevenshteinDistance(String, String) to + * getLevenshteinDistance(CharSequence, CharSequence) + */ + public static int getLevenshteinDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + /* + The difference between this impl. and the previous is that, rather + than creating and retaining a matrix of size s.length() + 1 by t.length() + 1, + we maintain two single-dimensional arrays of length s.length() + 1. The first, d, + is the 'current working' distance array that maintains the newest distance cost + counts as we iterate through the characters of String s. Each time we increment + the index of String t we are comparing, d is copied to p, the second int[]. Doing so + allows us to retain the previous cost counts as required by the algorithm (taking + the minimum of the cost count to the left, up one, and diagonally up and to the left + of the current cost count being calculated). (Note that the arrays aren't really + copied anymore, just switched...this is clearly much better than cloning an array + or doing a System.arraycopy() each time through the outer loop.) + + Effectively, the difference between the two implementations is this one does not + cause an out of memory condition when calculating the LD over two very large strings. + */ + + int n = s.length(); // length of s + int m = t.length(); // length of t + + if (n == 0) { + return m; + } else if (m == 0) { + return n; + } + + if (n > m) { + // swap the input strings to consume less memory + final CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n + 1]; //'previous' cost array, horizontally + int d[] = new int[n + 1]; // cost array, horizontally + int swap[]; //placeholder to assist in swapping p and d + + // indexes into strings s and t + int i; // iterates through s + int j; // iterates through t + + char tj; // jth character of t + + int cost; // cost + + for (i = 0; i <= n; i++) { + p[i] = i; + } + + for (j = 1; j <= m; j++) { + tj = t.charAt(j - 1); + d[0] = j; + + for (i = 1; i <= n; i++) { + cost = s.charAt(i - 1) == tj ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost); + } + + // copy current distance counts to 'previous row' distance counts + swap = p; + p = d; + d = swap; + } + + // our last action in the above loop was to switch d and p, so p now + // actually has the most recent cost counts + return p[n]; + } + + /** + *

Find the Levenshtein distance between two Strings if it's less than or equal to a given + * threshold.

+ *

+ *

This is the number of changes needed to change one String into + * another, where each change is a single character modification (deletion, + * insertion or substitution).

+ *

+ *

This implementation follows from Algorithms on Strings, Trees and Sequences by Dan Gusfield + * and Chas Emerick's implementation of the Levenshtein distance algorithm from + * http://www.merriampark.com/ld.htm

+ *

+ *

+     * StringUtils.getLevenshteinDistance(null, *, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, null, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, *, -1)               = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance("","", 0)               = 0
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 8)       = 7
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 7)       = 7
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 6))      = -1
+     * StringUtils.getLevenshteinDistance("elephant", "hippo", 7) = 7
+     * StringUtils.getLevenshteinDistance("elephant", "hippo", 6) = -1
+     * StringUtils.getLevenshteinDistance("hippo", "elephant", 7) = 7
+     * StringUtils.getLevenshteinDistance("hippo", "elephant", 6) = -1
+     * 
+ * + * @param s the first String, must not be null + * @param t the second String, must not be null + * @param threshold the target threshold, must not be negative + * @return result distance, or {@code -1} if the distance would be greater than the threshold + * @throws IllegalArgumentException if either String input {@code null} or negative threshold + */ + public static int getLevenshteinDistance(CharSequence s, CharSequence t, final int threshold) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + if (threshold < 0) { + throw new IllegalArgumentException("Threshold must not be negative"); + } + + /* + This implementation only computes the distance if it's less than or equal to the + threshold value, returning -1 if it's greater. The advantage is performance: unbounded + distance is O(nm), but a bound of k allows us to reduce it to O(km) time by only + computing a diagonal stripe of width 2k + 1 of the cost table. + It is also possible to use this to compute the unbounded Levenshtein distance by starting + the threshold at 1 and doubling each time until the distance is found; this is O(dm), where + d is the distance. + + One subtlety comes from needing to ignore entries on the border of our stripe + eg. + p[] = |#|#|#|* + d[] = *|#|#|#| + We must ignore the entry to the left of the leftmost member + We must ignore the entry above the rightmost member + + Another subtlety comes from our stripe running off the matrix if the strings aren't + of the same size. Since string s is always swapped to be the shorter of the two, + the stripe will always run off to the upper right instead of the lower left of the matrix. + + As a concrete example, suppose s is of length 5, t is of length 7, and our threshold is 1. + In this case we're going to walk a stripe of length 3. The matrix would look like so: + + 1 2 3 4 5 + 1 |#|#| | | | + 2 |#|#|#| | | + 3 | |#|#|#| | + 4 | | |#|#|#| + 5 | | | |#|#| + 6 | | | | |#| + 7 | | | | | | + + Note how the stripe leads off the table as there is no possible way to turn a string of length 5 + into one of length 7 in edit distance of 1. + + Additionally, this implementation decreases memory usage by using two + single-dimensional arrays and swapping them back and forth instead of allocating + an entire n by m matrix. This requires a few minor changes, such as immediately returning + when it's detected that the stripe has run off the matrix and initially filling the arrays with + large values so that entries we don't compute are ignored. + + See Algorithms on Strings, Trees and Sequences by Dan Gusfield for some discussion. + */ + + int n = s.length(); // length of s + int m = t.length(); // length of t + + // if one string is empty, the edit distance is necessarily the length of the other + if (n == 0) { + return m <= threshold ? m : -1; + } else if (m == 0) { + return n <= threshold ? n : -1; + } + + if (n > m) { + // swap the two strings to consume less memory + final CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n + 1]; // 'previous' cost array, horizontally + int d[] = new int[n + 1]; // cost array, horizontally + int _d[]; // placeholder to assist in swapping p and d + + // fill in starting table values + final int boundary = Math.min(n, threshold) + 1; + for (int i = 0; i < boundary; i++) { + p[i] = i; + } + // these fills ensure that the value above the rightmost entry of our + // stripe will be ignored in following loop iterations + Arrays.fill(p, boundary, p.length, Integer.MAX_VALUE); + Arrays.fill(d, Integer.MAX_VALUE); + + // iterates through t + for (int j = 1; j <= m; j++) { + final char t_j = t.charAt(j - 1); // jth character of t + d[0] = j; + + // compute stripe indices, constrain to array size + final int min = Math.max(1, j - threshold); + final int max = (j > Integer.MAX_VALUE - threshold) ? n : Math.min(n, j + threshold); + + // the stripe may lead off of the table if s and t are of different sizes + if (min > max) { + return -1; + } + + // ignore entry left of leftmost + if (min > 1) { + d[min - 1] = Integer.MAX_VALUE; + } + + // iterates through [min, max] in s + for (int i = min; i <= max; i++) { + if (s.charAt(i - 1) == t_j) { + // diagonally left and up + d[i] = p[i - 1]; + } else { + // 1 + minimum of cell to the left, to the top, diagonally left and up + d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]); + } + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + + // if p[n] is greater than the threshold, there's no guarantee on it being the correct + // distance + if (p[n] <= threshold) { + return p[n]; + } + return -1; + } + + /** + *

Find the Jaro Winkler Distance which indicates the similarity score between two Strings.

+ *

+ *

The Jaro measure is the weighted sum of percentage of matched characters from each file and transposed characters. + * Winkler increased this measure for matching initial characters.

+ *

+ *

This implementation is based on the Jaro Winkler similarity algorithm + * from http://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance.

+ *

+ *

+     * StringUtils.getJaroWinklerDistance(null, null)          = IllegalArgumentException
+     * StringUtils.getJaroWinklerDistance("","")               = 0.0
+     * StringUtils.getJaroWinklerDistance("","a")              = 0.0
+     * StringUtils.getJaroWinklerDistance("aaapppp", "")       = 0.0
+     * StringUtils.getJaroWinklerDistance("frog", "fog")       = 0.93
+     * StringUtils.getJaroWinklerDistance("fly", "ant")        = 0.0
+     * StringUtils.getJaroWinklerDistance("elephant", "hippo") = 0.44
+     * StringUtils.getJaroWinklerDistance("hippo", "elephant") = 0.44
+     * StringUtils.getJaroWinklerDistance("hippo", "zzzzzzzz") = 0.0
+     * StringUtils.getJaroWinklerDistance("hello", "hallo")    = 0.88
+     * StringUtils.getJaroWinklerDistance("ABC Corporation", "ABC Corp") = 0.91
+     * StringUtils.getJaroWinklerDistance("D N H Enterprises Inc", "D & H Enterprises, Inc.") = 0.93
+     * StringUtils.getJaroWinklerDistance("My Gym Children's Fitness Center", "My Gym. Childrens Fitness") = 0.94
+     * StringUtils.getJaroWinklerDistance("PENNSYLVANIA", "PENNCISYLVNIA")    = 0.9
+     * 
+ * + * @param first the first String, must not be null + * @param second the second String, must not be null + * @return result distance + * @throws IllegalArgumentException if either String input {@code null} + * @since 3.3 + */ + public static double getJaroWinklerDistance(final CharSequence first, final CharSequence second) { + final double DEFAULT_SCALING_FACTOR = 0.1; + + if (first == null || second == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + final double jaro = score(first, second); + final int cl = commonPrefixLength(first, second); + final double matchScore = Math.round((jaro + (DEFAULT_SCALING_FACTOR * cl * (1.0 - jaro))) * 100.0) / 100.0; + + return matchScore; + } + + /** + * This method returns the Jaro-Winkler score for string matching. + * + * @param first the first string to be matched + * @param second the second string to be machted + * @return matching score without scaling factor impact + */ + private static double score(final CharSequence first, final CharSequence second) { + String shorter; + String longer; + + // Determine which String is longer. + if (first.length() > second.length()) { + longer = first.toString().toLowerCase(); + shorter = second.toString().toLowerCase(); + } else { + longer = second.toString().toLowerCase(); + shorter = first.toString().toLowerCase(); + } + + // Calculate the half length() distance of the shorter String. + final int halflength = (shorter.length() / 2) + 1; + + // Find the set of matching characters between the shorter and longer strings. Note that + // the set of matching characters may be different depending on the order of the strings. + final String m1 = getSetOfMatchingCharacterWithin(shorter, longer, halflength); + final String m2 = getSetOfMatchingCharacterWithin(longer, shorter, halflength); + + // If one or both of the sets of common characters is empty, then + // there is no similarity between the two strings. + if (m1.length() == 0 || m2.length() == 0) { + return 0.0; + } + + // If the set of common characters is not the same size, then + // there is no similarity between the two strings, either. + if (m1.length() != m2.length()) { + return 0.0; + } + + // Calculate the number of transposition between the two sets + // of common characters. + final int transpositions = transpositions(m1, m2); + + // Calculate the distance. + final double dist = + (m1.length() / ((double) shorter.length()) + + m2.length() / ((double) longer.length()) + + (m1.length() - transpositions) / ((double) m1.length())) / 3.0; + return dist; + } + + /** + * Gets a set of matching characters between two strings. + *

+ *

+ * + * @param first The first string. + * @param second The second string. + * @param limit The maximum distance to consider. + * @return A string contain the set of common characters. + */ + private static String getSetOfMatchingCharacterWithin(final CharSequence first, final CharSequence second, final int limit) { + final StringBuilder common = new StringBuilder(); + final StringBuilder copy = new StringBuilder(second); + + for (int i = 0; i < first.length(); i++) { + final char ch = first.charAt(i); + boolean found = false; + + // See if the character is within the limit positions away from the original position of that character. + for (int j = Math.max(0, i - limit); !found && j < Math.min(i + limit, second.length()); j++) { + if (copy.charAt(j) == ch) { + found = true; + common.append(ch); + copy.setCharAt(j, '*'); + } + } + } + return common.toString(); + } + + /** + * Calculates the number of transposition between two strings. + * + * @param first The first string. + * @param second The second string. + * @return The number of transposition between the two strings. + */ + private static int transpositions(CharSequence first, CharSequence second) { + int transpositions = 0; + for (int i = 0; i < first.length(); i++) { + if (first.charAt(i) != second.charAt(i)) { + transpositions++; + } + } + return transpositions / 2; + } + + /** + * Calculates the number of characters from the beginning of the strings that match exactly one-to-one, + * up to a maximum of four (4) characters. + * + * @param first The first string. + * @param second The second string. + * @return A number between 0 and 4. + */ + private static int commonPrefixLength(CharSequence first, CharSequence second) { + final int result = getCommonPrefix(first.toString(), second.toString()).length(); + + // Limit the result to 4. + return result > 4 ? 4 : result; + } + + // startsWith + //----------------------------------------------------------------------- + + /** + *

Check if a CharSequence starts with a specified prefix.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ *

+ *

+     * StringUtils.startsWith(null, null)      = true
+     * StringUtils.startsWith(null, "abc")     = false
+     * StringUtils.startsWith("abcdef", null)  = false
+     * StringUtils.startsWith("abcdef", "abc") = true
+     * StringUtils.startsWith("ABCDEF", "abc") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @return {@code true} if the CharSequence starts with the prefix, case sensitive, or + * both {@code null} + * @see String#startsWith(String) + * @since 3.0 Changed signature from startsWith(String, String) to startsWith(CharSequence, CharSequence) + */ + public static boolean startsWith(final CharSequence str, final CharSequence prefix) { + return startsWith(str, prefix, false); + } + + /** + *

Case insensitive check if a CharSequence starts with a specified prefix.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case insensitive.

+ *

+ *

+     * StringUtils.startsWithIgnoreCase(null, null)      = true
+     * StringUtils.startsWithIgnoreCase(null, "abc")     = false
+     * StringUtils.startsWithIgnoreCase("abcdef", null)  = false
+     * StringUtils.startsWithIgnoreCase("abcdef", "abc") = true
+     * StringUtils.startsWithIgnoreCase("ABCDEF", "abc") = true
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @return {@code true} if the CharSequence starts with the prefix, case insensitive, or + * both {@code null} + * @see String#startsWith(String) + * @since 3.0 Changed signature from startsWithIgnoreCase(String, String) to startsWithIgnoreCase(CharSequence, CharSequence) + */ + public static boolean startsWithIgnoreCase(final CharSequence str, final CharSequence prefix) { + return startsWith(str, prefix, true); + } + + /** + *

Check if a CharSequence starts with a specified prefix (optionally case insensitive).

+ * + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @param ignoreCase indicates whether the compare should ignore case + * (case insensitive) or not. + * @return {@code true} if the CharSequence starts with the prefix or + * both {@code null} + * @see String#startsWith(String) + */ + private static boolean startsWith(final CharSequence str, final CharSequence prefix, final boolean ignoreCase) { + if (str == null || prefix == null) { + return str == null && prefix == null; + } + if (prefix.length() > str.length()) { + return false; + } + return CharSequenceUtils.regionMatches(str, ignoreCase, 0, prefix, 0, prefix.length()); + } + + /** + *

Check if a CharSequence starts with any of an array of specified strings.

+ *

+ *

+     * StringUtils.startsWithAny(null, null)      = false
+     * StringUtils.startsWithAny(null, new String[] {"abc"})  = false
+     * StringUtils.startsWithAny("abcxyz", null)     = false
+     * StringUtils.startsWithAny("abcxyz", new String[] {""}) = false
+     * StringUtils.startsWithAny("abcxyz", new String[] {"abc"}) = true
+     * StringUtils.startsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
+     * 
+ * + * @param string the CharSequence to check, may be null + * @param searchStrings the CharSequences to find, may be null or empty + * @return {@code true} if the CharSequence starts with any of the the prefixes, case insensitive, or + * both {@code null} + * @since 3.0 Changed signature from startsWithAny(String, String[]) to startsWithAny(CharSequence, CharSequence...) + */ + public static boolean startsWithAny(final CharSequence string, final CharSequence... searchStrings) { + if (isEmpty(string) || ArrayUtils.isEmpty(searchStrings)) { + return false; + } + for (final CharSequence searchString : searchStrings) { + if (StringUtils.startsWith(string, searchString)) { + return true; + } + } + return false; + } + + // endsWith + //----------------------------------------------------------------------- + + /** + *

Check if a CharSequence ends with a specified suffix.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ *

+ *

+     * StringUtils.endsWith(null, null)      = true
+     * StringUtils.endsWith(null, "def")     = false
+     * StringUtils.endsWith("abcdef", null)  = false
+     * StringUtils.endsWith("abcdef", "def") = true
+     * StringUtils.endsWith("ABCDEF", "def") = false
+     * StringUtils.endsWith("ABCDEF", "cde") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @return {@code true} if the CharSequence ends with the suffix, case sensitive, or + * both {@code null} + * @see String#endsWith(String) + * @since 3.0 Changed signature from endsWith(String, String) to endsWith(CharSequence, CharSequence) + */ + public static boolean endsWith(final CharSequence str, final CharSequence suffix) { + return endsWith(str, suffix, false); + } + + /** + *

Case insensitive check if a CharSequence ends with a specified suffix.

+ *

+ *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case insensitive.

+ *

+ *

+     * StringUtils.endsWithIgnoreCase(null, null)      = true
+     * StringUtils.endsWithIgnoreCase(null, "def")     = false
+     * StringUtils.endsWithIgnoreCase("abcdef", null)  = false
+     * StringUtils.endsWithIgnoreCase("abcdef", "def") = true
+     * StringUtils.endsWithIgnoreCase("ABCDEF", "def") = true
+     * StringUtils.endsWithIgnoreCase("ABCDEF", "cde") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @return {@code true} if the CharSequence ends with the suffix, case insensitive, or + * both {@code null} + * @see String#endsWith(String) + * @since 3.0 Changed signature from endsWithIgnoreCase(String, String) to endsWithIgnoreCase(CharSequence, CharSequence) + */ + public static boolean endsWithIgnoreCase(final CharSequence str, final CharSequence suffix) { + return endsWith(str, suffix, true); + } + + /** + *

Check if a CharSequence ends with a specified suffix (optionally case insensitive).

+ * + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @param ignoreCase indicates whether the compare should ignore case + * (case insensitive) or not. + * @return {@code true} if the CharSequence starts with the prefix or + * both {@code null} + * @see String#endsWith(String) + */ + private static boolean endsWith(final CharSequence str, final CharSequence suffix, final boolean ignoreCase) { + if (str == null || suffix == null) { + return str == null && suffix == null; + } + if (suffix.length() > str.length()) { + return false; + } + final int strOffset = str.length() - suffix.length(); + return CharSequenceUtils.regionMatches(str, ignoreCase, strOffset, suffix, 0, suffix.length()); + } + + /** + *

+ * Similar to http://www.w3.org/TR/xpath/#function-normalize + * -space + *

+ *

+ * The function returns the argument string with whitespace normalized by using + * {@link #trim(String)} to remove leading and trailing whitespace + * and then replacing sequences of whitespace characters by a single space. + *

+ * In XML Whitespace characters are the same as those allowed by the S production, which is S ::= (#x20 | #x9 | #xD | #xA)+ + *

+ * Java's regexp pattern \s defines whitespace as [ \t\n\x0B\f\r] + *

+ *

For reference:

+ *
    + *
  • \x0B = vertical tab
  • + *
  • \f = #xC = form feed
  • + *
  • #x20 = space
  • + *
  • #x9 = \t
  • + *
  • #xA = \n
  • + *
  • #xD = \r
  • + *
+ *

+ *

+ * The difference is that Java's whitespace includes vertical tab and form feed, which this functional will also + * normalize. Additionally {@link #trim(String)} removes control characters (char <= 32) from both + * ends of this String. + *

+ * + * @param str the source String to normalize whitespaces from, may be null + * @return the modified string with whitespace normalized, {@code null} if null String input + * @see Pattern + * @see #trim(String) + * @see http://www.w3.org/TR/xpath/#function-normalize-space + * @since 3.0 + */ + public static String normalizeSpace(final String str) { + if (str == null) { + return null; + } + return WHITESPACE_PATTERN.matcher(trim(str)).replaceAll(SPACE); + } + + /** + *

Check if a CharSequence ends with any of an array of specified strings.

+ *

+ *

+     * StringUtils.endsWithAny(null, null)      = false
+     * StringUtils.endsWithAny(null, new String[] {"abc"})  = false
+     * StringUtils.endsWithAny("abcxyz", null)     = false
+     * StringUtils.endsWithAny("abcxyz", new String[] {""}) = true
+     * StringUtils.endsWithAny("abcxyz", new String[] {"xyz"}) = true
+     * StringUtils.endsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
+     * 
+ * + * @param string the CharSequence to check, may be null + * @param searchStrings the CharSequences to find, may be null or empty + * @return {@code true} if the CharSequence ends with any of the the prefixes, case insensitive, or + * both {@code null} + * @since 3.0 + */ + public static boolean endsWithAny(final CharSequence string, final CharSequence... searchStrings) { + if (isEmpty(string) || ArrayUtils.isEmpty(searchStrings)) { + return false; + } + for (final CharSequence searchString : searchStrings) { + if (StringUtils.endsWith(string, searchString)) { + return true; + } + } + return false; + } + + /** + * Appends the suffix to the end of the string if the string does not + * already end in the suffix. + * + * @param str The string. + * @param suffix The suffix to append to the end of the string. + * @param ignoreCase Indicates whether the compare should ignore case. + * @param suffixes Additional suffixes that are valid terminators (optional). + * @return A new String if suffix was appened, the same string otherwise. + */ + private static String appendIfMissing(final String str, final CharSequence suffix, final boolean ignoreCase, final CharSequence... suffixes) { + if (str == null || isEmpty(suffix) || endsWith(str, suffix, ignoreCase)) { + return str; + } + if (suffixes != null && suffixes.length > 0) { + for (final CharSequence s : suffixes) { + if (endsWith(str, s, ignoreCase)) { + return str; + } + } + } + return str + suffix.toString(); + } + + /** + * Appends the suffix to the end of the string if the string does not + * already end with any the suffixes. + *

+ *

+     * StringUtils.appendIfMissing(null, null) = null
+     * StringUtils.appendIfMissing("abc", null) = "abc"
+     * StringUtils.appendIfMissing("", "xyz") = "xyz"
+     * StringUtils.appendIfMissing("abc", "xyz") = "abcxyz"
+     * StringUtils.appendIfMissing("abcxyz", "xyz") = "abcxyz"
+     * StringUtils.appendIfMissing("abcXYZ", "xyz") = "abcXYZxyz"
+     * 
+ *

With additional suffixes,

+ *
+     * StringUtils.appendIfMissing(null, null, null) = null
+     * StringUtils.appendIfMissing("abc", null, null) = "abc"
+     * StringUtils.appendIfMissing("", "xyz", null) = "xyz"
+     * StringUtils.appendIfMissing("abc", "xyz", new CharSequence[]{null}) = "abcxyz"
+     * StringUtils.appendIfMissing("abc", "xyz", "") = "abc"
+     * StringUtils.appendIfMissing("abc", "xyz", "mno") = "abcxyz"
+     * StringUtils.appendIfMissing("abcxyz", "xyz", "mno") = "abcxyz"
+     * StringUtils.appendIfMissing("abcmno", "xyz", "mno") = "abcmno"
+     * StringUtils.appendIfMissing("abcXYZ", "xyz", "mno") = "abcXYZxyz"
+     * StringUtils.appendIfMissing("abcMNO", "xyz", "mno") = "abcMNOxyz"
+     * 
+ * + * @param str The string. + * @param suffix The suffix to append to the end of the string. + * @param suffixes Additional suffixes that are valid terminators. + * @return A new String if suffix was appened, the same string otherwise. + * @since 3.2 + */ + public static String appendIfMissing(final String str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, false, suffixes); + } + + /** + * Appends the suffix to the end of the string if the string does not + * already end, case insensitive, with any of the suffixes. + *

+ *

+     * StringUtils.appendIfMissingIgnoreCase(null, null) = null
+     * StringUtils.appendIfMissingIgnoreCase("abc", null) = "abc"
+     * StringUtils.appendIfMissingIgnoreCase("", "xyz") = "xyz"
+     * StringUtils.appendIfMissingIgnoreCase("abc", "xyz") = "abcxyz"
+     * StringUtils.appendIfMissingIgnoreCase("abcxyz", "xyz") = "abcxyz"
+     * StringUtils.appendIfMissingIgnoreCase("abcXYZ", "xyz") = "abcXYZ"
+     * 
+ *

With additional suffixes,

+ *
+     * StringUtils.appendIfMissingIgnoreCase(null, null, null) = null
+     * StringUtils.appendIfMissingIgnoreCase("abc", null, null) = "abc"
+     * StringUtils.appendIfMissingIgnoreCase("", "xyz", null) = "xyz"
+     * StringUtils.appendIfMissingIgnoreCase("abc", "xyz", new CharSequence[]{null}) = "abcxyz"
+     * StringUtils.appendIfMissingIgnoreCase("abc", "xyz", "") = "abc"
+     * StringUtils.appendIfMissingIgnoreCase("abc", "xyz", "mno") = "axyz"
+     * StringUtils.appendIfMissingIgnoreCase("abcxyz", "xyz", "mno") = "abcxyz"
+     * StringUtils.appendIfMissingIgnoreCase("abcmno", "xyz", "mno") = "abcmno"
+     * StringUtils.appendIfMissingIgnoreCase("abcXYZ", "xyz", "mno") = "abcXYZ"
+     * StringUtils.appendIfMissingIgnoreCase("abcMNO", "xyz", "mno") = "abcMNO"
+     * 
+ * + * @param str The string. + * @param suffix The suffix to append to the end of the string. + * @param suffixes Additional suffixes that are valid terminators. + * @return A new String if suffix was appened, the same string otherwise. + * @since 3.2 + */ + public static String appendIfMissingIgnoreCase(final String str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, true, suffixes); + } + + /** + * Prepends the prefix to the start of the string if the string does not + * already start with any of the prefixes. + * + * @param str The string. + * @param prefix The prefix to prepend to the start of the string. + * @param ignoreCase Indicates whether the compare should ignore case. + * @param prefixes Additional prefixes that are valid (optional). + * @return A new String if prefix was prepended, the same string otherwise. + */ + private static String prependIfMissing(final String str, final CharSequence prefix, final boolean ignoreCase, final CharSequence... prefixes) { + if (str == null || isEmpty(prefix) || startsWith(str, prefix, ignoreCase)) { + return str; + } + if (prefixes != null && prefixes.length > 0) { + for (final CharSequence p : prefixes) { + if (startsWith(str, p, ignoreCase)) { + return str; + } + } + } + return prefix.toString() + str; + } + + /** + * Prepends the prefix to the start of the string if the string does not + * already start with any of the prefixes. + *

+ *

+     * StringUtils.prependIfMissing(null, null) = null
+     * StringUtils.prependIfMissing("abc", null) = "abc"
+     * StringUtils.prependIfMissing("", "xyz") = "xyz"
+     * StringUtils.prependIfMissing("abc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissing("xyzabc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissing("XYZabc", "xyz") = "xyzXYZabc"
+     * 
+ *

With additional prefixes,

+ *
+     * StringUtils.prependIfMissing(null, null, null) = null
+     * StringUtils.prependIfMissing("abc", null, null) = "abc"
+     * StringUtils.prependIfMissing("", "xyz", null) = "xyz"
+     * StringUtils.prependIfMissing("abc", "xyz", new CharSequence[]{null}) = "xyzabc"
+     * StringUtils.prependIfMissing("abc", "xyz", "") = "abc"
+     * StringUtils.prependIfMissing("abc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissing("xyzabc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissing("mnoabc", "xyz", "mno") = "mnoabc"
+     * StringUtils.prependIfMissing("XYZabc", "xyz", "mno") = "xyzXYZabc"
+     * StringUtils.prependIfMissing("MNOabc", "xyz", "mno") = "xyzMNOabc"
+     * 
+ * + * @param str The string. + * @param prefix The prefix to prepend to the start of the string. + * @param prefixes Additional prefixes that are valid. + * @return A new String if prefix was prepended, the same string otherwise. + * @since 3.2 + */ + public static String prependIfMissing(final String str, final CharSequence prefix, final CharSequence... prefixes) { + return prependIfMissing(str, prefix, false, prefixes); + } + + /** + * Prepends the prefix to the start of the string if the string does not + * already start, case insensitive, with any of the prefixes. + *

+ *

+     * StringUtils.prependIfMissingIgnoreCase(null, null) = null
+     * StringUtils.prependIfMissingIgnoreCase("abc", null) = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("", "xyz") = "xyz"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("xyzabc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("XYZabc", "xyz") = "XYZabc"
+     * 
+ *

With additional prefixes,

+ *
+     * StringUtils.prependIfMissingIgnoreCase(null, null, null) = null
+     * StringUtils.prependIfMissingIgnoreCase("abc", null, null) = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("", "xyz", null) = "xyz"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", new CharSequence[]{null}) = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", "") = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("xyzabc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("mnoabc", "xyz", "mno") = "mnoabc"
+     * StringUtils.prependIfMissingIgnoreCase("XYZabc", "xyz", "mno") = "XYZabc"
+     * StringUtils.prependIfMissingIgnoreCase("MNOabc", "xyz", "mno") = "MNOabc"
+     * 
+ * + * @param str The string. + * @param prefix The prefix to prepend to the start of the string. + * @param prefixes Additional prefixes that are valid (optional). + * @return A new String if prefix was prepended, the same string otherwise. + * @since 3.2 + */ + public static String prependIfMissingIgnoreCase(final String str, final CharSequence prefix, final CharSequence... prefixes) { + return prependIfMissing(str, prefix, true, prefixes); + } + + /** + * Converts a byte[] to a String using the specified character encoding. + * + * @param bytes the byte array to read from + * @param charsetName the encoding to use, if null then use the platform default + * @return a new String + * @throws UnsupportedEncodingException If the named charset is not supported + * @throws NullPointerException if the input is null + * @since 3.1 + * @deprecated use {@link StringUtils#toEncodedString(byte[], Charset)} instead of String constants in your code + */ + @Deprecated + public static String toString(final byte[] bytes, final String charsetName) throws UnsupportedEncodingException { + return charsetName != null ? new String(bytes, charsetName) : new String(bytes, Charset.defaultCharset()); + } + + /** + * Converts a byte[] to a String using the specified character encoding. + * + * @param bytes the byte array to read from + * @param charset the encoding to use, if null then use the platform default + * @return a new String + * @throws NullPointerException if {@code bytes} is null + * @since 3.3 No longer throws {@link UnsupportedEncodingException}. + */ + public static String toEncodedString(byte[] bytes, Charset charset) { + return new String(bytes, charset != null ? charset : Charset.defaultCharset()); + } + + public static String filterSpecificChar(String str) throws Exception{ + // 清除掉所有特殊字符 + String regEx = "[`~!@#$%^&*()+=|{}':;',//[//].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]"; + Pattern p = Pattern.compile(regEx); + Matcher m = p.matcher(str); + return m.replaceAll("").trim(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/SystemUtils.java b/library/src/main/java/com/chwl/library/utils/SystemUtils.java new file mode 100644 index 0000000..4d069f9 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/SystemUtils.java @@ -0,0 +1,55 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.os.Build; + +/** + *

+ * + * @author jiahui + * @date 2017/12/16 + */ +public class SystemUtils { + + /** + * 获取手机型号 + * + * @return + */ + public static String getPhoneModel() { + return Build.MODEL; + } + + /** + * 获取是否nettype字段 + * + * @return + */ + public static int getNetworkType(Context context) { + if (NetworkUtils.getNetworkType(context) == NetworkUtils.NET_WIFI) { + return 2; + } else { + return 1; + } + } + + /** + * 获取运营商字段 + * + * @return + */ + public static int getIspType(Context context) { + String isp = NetworkUtils.getOperator(context); + int ispType = 4; + if (isp.equals(NetworkUtils.ChinaOperator.CMCC)) { + ispType = 1; + } else if (isp.equals(NetworkUtils.ChinaOperator.UNICOM)) { + ispType = 2; + } else if (isp.equals(NetworkUtils.ChinaOperator.CTL)) { + ispType = 3; + } + + return ispType; + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/TextWatcherWrapper.java b/library/src/main/java/com/chwl/library/utils/TextWatcherWrapper.java new file mode 100644 index 0000000..c7e06c6 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/TextWatcherWrapper.java @@ -0,0 +1,21 @@ +package com.chwl.library.utils; + +import android.text.Editable; +import android.text.TextWatcher; + +public class TextWatcherWrapper implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + + } +} diff --git a/library/src/main/java/com/chwl/library/utils/TimeUtils.java b/library/src/main/java/com/chwl/library/utils/TimeUtils.java new file mode 100644 index 0000000..cf81d12 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/TimeUtils.java @@ -0,0 +1,1000 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.utils.log.MLog; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.IllegalFormatException; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Created by Zhanghuiping on 14/6/5. + */ +public class TimeUtils { + + public static final String RULE_1 = "yyyy-MM-dd"; + public static final String RULE_2 = "yyyyMMdd"; + + public static final long MONTHS_OF_YEAR = 12; + public static final long DAYS_OF_YEAR = 365; + public static final long DAYS_OF_MONTH = 30; + public static final long HOURS_OF_DAY = 24; + public static final long MINUTES_OF_HOUR = 60; + public static final long SECONDS_OF_MINUTE = 60; + public static final long MILLIS_OF_SECOND = 1000; + /** + * 1分钟的毫秒数 + */ + public static final long MILLIS_OF_A_MINUTE = SECONDS_OF_MINUTE + * MILLIS_OF_SECOND; + /** + * 1小时的毫秒数 + */ + public static final long MILLIS_OF_A_HOUR = MINUTES_OF_HOUR * SECONDS_OF_MINUTE + * MILLIS_OF_SECOND; + /** + * 一天的毫秒数 + */ + public static final long MILLIS_OF_A_DAY = + HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE * MILLIS_OF_SECOND; + + public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final long MILLIS_OF_SEC = 1000; + + public static String getCurrentDateStr() { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH); + return dateFormat.format(new Date()); + } + + public static String getCurrentTimeStr() { + SimpleDateFormat dateFormat = new SimpleDateFormat(TIME_FORMAT, Locale.ENGLISH); + return dateFormat.format(new Date()); + } + + public static boolean isSameDay(long millis1, long millis2) { + Calendar c1 = Calendar.getInstance(); + c1.setTimeInMillis(millis1); + + Calendar c2 = Calendar.getInstance(); + c2.setTimeInMillis(millis2); + + return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) + && c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH) + && c1.get(Calendar.DAY_OF_MONTH) == c2.get(Calendar.DAY_OF_MONTH); + } + + public static int isYesterday(String date) { + int day = 0; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + try { + Date d1 = new Date();//当前时间 + Date d2 = sdf.parse(date);//传进的时间 + long cha = d2.getTime() - d1.getTime(); + day = Long.valueOf(cha / (1000 * 60 * 60 * 24)).intValue(); + } catch (ParseException e) { + e.printStackTrace(); + } + + return day; + + } + + public static int getYear(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.YEAR); + } + + public static int getMonth(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.MONTH) + 1; + } + + public static int getDayOfMonth(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.DAY_OF_MONTH); + } + + public static int getDayOfWeek(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.DAY_OF_WEEK); + } + + /** + * @param millis time in millis + * @return hour of 12-hour clock + */ + public static int getHourOf12HClock(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.HOUR); + } + + /** + * @param millis time in millis + * @return hour of 24-hour clock + */ + public static int getHourOf24HClock(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.HOUR_OF_DAY); + } + + public static int getMinute(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.MINUTE); + } + + public static int getSecond(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + return c.get(Calendar.SECOND); + } + + public static long nextDay(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + c.add(Calendar.DATE, 1); // number of days to add + return c.getTimeInMillis(); + } + + public static long previousDay(long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + c.add(Calendar.DATE, -1); // number of days to add + return c.getTimeInMillis(); + } + + /** + * Get current time in secs. + * + * @return Current time in seconds. + */ + public static int curSec() { + long l = System.currentTimeMillis(); + return (int) (l / MILLIS_OF_SEC); + } + + /** + * Get expired dead time, in millis. + * + * @param millisToExpire millis from now to the dead time. + * @return Expired dead time, in millis, it is in UTC(GMT). + */ + public static long getExpireDeadTime(long millisToExpire) { + return System.currentTimeMillis() + millisToExpire; + } + + /** + * @param timeMillis time in millis. + * @return format time string , like "2014-06-06 16:19:15" + */ + public static String getTimeStringFromMillis(long timeMillis) { + String format = "%04d-%02d-%02d %02d:%02d:%02d"; + return getTimeStringFromMillis(timeMillis, format); + } + + /** + * @param timeMillis time in millis + * @param format format string, must contain 6 args: year, month, day, hour, minute, second + * @return format time string + */ + public static String getTimeStringFromMillis(long timeMillis, String format) { + if (format == null || format.length() == 0) { + return null; + } + final Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("GMT+8")); + cal.setTimeInMillis(timeMillis); + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH) + 1; + int day = cal.get(Calendar.DAY_OF_MONTH); + int hour = cal.get(Calendar.HOUR_OF_DAY); + int min = cal.get(Calendar.MINUTE); + int sec = cal.get(Calendar.SECOND); + String timeString = null; + try { + timeString = String.format(Locale.ENGLISH, format, year, month, day, hour, min, sec); + } catch (IllegalFormatException e) { + MLog.error("TimeUtils", "getTimeStringFromMillis error! " + e.toString()); + } + return timeString; + } + + //毫秒值转换成年月日 + public static String getDateTimeString(long milliseconds, String format) { + String result = null; + try { + SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ENGLISH); + Date date = new Date(milliseconds); + result = formatter.format(date); + } catch (Exception ex) { + ex.printStackTrace(); + } + if (result == null) { + result = ""; + } + return result; + } + + public static long getTimeMillis(String time, String format) { + try { + SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ENGLISH); + Date date = formatter.parse(time); + return date.getTime(); + } catch (Exception ex) { + ex.printStackTrace(); + } + + return 0; + } + + /** + * 将毫秒数转为 MM:ss 的格式 + * + * @param millisecond 毫秒数 + * @return + */ + public static String ms2MS(long millisecond) { + return ms2MS(millisecond, true); + } + + /** + * 将毫秒数转为 MM:ss 的格式 + * + * @param millisecond 毫秒数 + * @return + */ + public static String ms2MS(long millisecond, boolean isAdd) { + millisecond /= 1000; + long minute = millisecond / 60; + long second = millisecond % 60; + String minuteString = String.valueOf(minute); + String secondString = String.valueOf(second); + if (isAdd && minute < 10) { + minuteString = "0" + minuteString; + } + if (second < 10) { + secondString = "0" + secondString; + } + return minuteString + ":" + secondString; + } + + /** + * 将毫秒时间转换为指定的字符串格式,例如传入format“year-mon-day”,返回类似“2014-08-29” + * + * @param timeMillis + * @param format year,mon,day,hour,min,sec + * @return 指定时间格式 + */ + public static String getFormatTimeString(long timeMillis, String format) { + if (format == null || format.length() == 0) { + return null; + } + final Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(timeMillis); + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH) + 1; + int day = cal.get(Calendar.DAY_OF_MONTH); + int hour = cal.get(Calendar.HOUR_OF_DAY); + int min = cal.get(Calendar.MINUTE); + int sec = cal.get(Calendar.SECOND); + String timeString = null; + try { +// timeString = String.format(format, year, month, day, hour, min, sec); + timeString = format.replaceAll("year", String.valueOf(year)) + .replaceAll("mon", month < 10 ? "0" + month : "" + month) + .replaceAll("day", day < 10 ? "0" + day : "" + day) + .replaceAll("hour", hour < 10 ? "0" + hour : "" + hour) + .replaceAll("min", min < 10 ? "0" + min : "" + min) + .replaceAll("sec", sec < 10 ? "0" + sec : "" + sec); + } catch (Exception e) { + MLog.error("TimeUtils", "getFormatTimeString error! " + e.toString()); + } + return timeString; + } + + public static String getPostTimeString(Context context, long timeMillis, boolean showToday, boolean showSecond) { + if (context == null) { + return null; + } + String sSpace = " "; + String sToday = context.getString(R.string.str_today); + String sRightNow = context.getString(R.string.str_right_now); + String sYesterday = context.getString(R.string.str_yesterday); + String sBeforeYesterday = context.getString(R.string.str_day_before_yesterday); + String sShortDateFormat = context.getString(R.string.str_short_date_format); + String sDateFormat = context.getString(R.string.str_date_format); + String sHourAgo = context.getString(R.string.str_hours_ago_format); + String sMinutAgo = context.getString(R.string.str_minutes_ago_format); + + Calendar current = Calendar.getInstance(); + Calendar post = Calendar.getInstance(); + + current.setTimeInMillis(System.currentTimeMillis()); + post.setTimeInMillis(timeMillis); + + int diffSecs = (int) ((current.getTimeInMillis() - post.getTimeInMillis()) / MILLIS_OF_SECOND); + if (diffSecs <= 0) { + return sRightNow; + } + int diffHours = (int) (diffSecs / SECONDS_OF_MINUTE / MINUTES_OF_HOUR); + int diffDays = current.get(Calendar.DAY_OF_YEAR) - post.get(Calendar.DAY_OF_YEAR); + + boolean isSameYear = post.get(Calendar.YEAR) == current.get(Calendar.YEAR); + boolean isSameDay = diffHours <= HOURS_OF_DAY; + + StringBuilder builder = new StringBuilder(); + if (!isSameDay || !isSameYear) { + if (diffDays > 0 && diffDays <= 2) { + builder.append(diffDays == 1 ? sYesterday : sBeforeYesterday); + builder.append(sSpace); + } else { + if (isSameYear) { + builder.append(String.format(sShortDateFormat, + post.get(Calendar.MONTH) + 1, + post.get(Calendar.DAY_OF_MONTH))); + builder.append(sSpace); + } else { + builder.append(String.format(sDateFormat, + post.get(Calendar.YEAR), + post.get(Calendar.MONTH) + 1, + post.get(Calendar.DAY_OF_MONTH))); + builder.append(sSpace); + } + } + } else if (showToday) { + + if (diffHours > 0) { + builder.append(String.format(sHourAgo, diffHours)); + } else { + int diffMinuts = diffSecs / 60; + if (diffMinuts > 0) { + builder.append(String.format(sMinutAgo, diffMinuts)); + } else { + builder.append(String.format(sMinutAgo, diffMinuts + 1)); + } + } +// builder.append(sToday); +// builder.append(sSpace); + } + +// if (showSecond) { +// builder.append(String.format(context.getString(R.string.str_time_format), +// post.get(Calendar.HOUR_OF_DAY), post.get(Calendar.MINUTE), post.get(Calendar.SECOND))); +// } else { +// builder.append(String.format(context.getString(R.string.str_short_time_format), +// post.get(Calendar.HOUR_OF_DAY), post.get(Calendar.MINUTE))); +// } + return builder.toString(); + } + + public static String timeConversion(long time) { + long hour = 0; + long minutes = 0; + long sencond = 0; + long temp = time % 3600; + if (time > 3600) { + hour = time / 3600; + if (temp != 0) { + if (temp > 60) { + minutes = temp / 60; + if (temp % 60 != 0) { + sencond = temp % 60; + } + } else { + sencond = temp; + } + } + } else { + minutes = time / 60; + if (time % 60 != 0) { + sencond = time % 60; + } + } + return (hour < 10 ? ("0" + hour) : hour) + ":" + (minutes < 10 ? ("0" + minutes) : minutes) + ":" + (sencond < 10 ? ("0" + sencond) : sencond); + } + + public static String + secondsToTime(long seconds) { + long h = seconds / 3600; //小时 + long m = (seconds % 3600) / 60; //分钟 + long s = (seconds % 3600) % 60; //秒 + if (h > 0) { + if (m == 0 && s == 0) { + return h + "小時"; + } else if (m != 0 && s == 0) { + return h + "小時" + m + "分鐘"; + } else { + return h + "小時" + m + "分鐘" + s + "秒"; + } + } + if (m > 0) { + if (s == 0) { + return m + "分鐘"; + } else { + return m + "分鐘" + s + "秒"; + } + } + return s + "秒"; + } + + public static String getChineseMonth(Context context, long millis) { + int month = getMonth(millis); + String[] monthStrs = context.getResources().getStringArray(R.array.time_month_strs); + if (!BlankUtil.isBlank(monthStrs)) { + for (int i = 0; i < monthStrs.length; i++) { + if ((month - 1) == i) { + return monthStrs[i]; + } + } + } + + return ""; + } + + /** + * @param datetime + * @param rule "yyyyMMdd" + * @return + */ + public static long dateString2Time(String datetime, String rule) { + SimpleDateFormat dateFormat = new SimpleDateFormat(rule); + try { + Date date = dateFormat.parse(datetime); + MLog.debug(TimeUtils.class, "" + date.getTime()); + return date.getTime(); + } catch (ParseException e) { + e.printStackTrace(); + MLog.error(TimeUtils.class, e); + } + + return 0; + } + + /** + * t multiply scale, check overflow + * + * @param t t + * @param scale scale + * @return t * scale if not overflow, else return Long.MAX_VALUE or Long.MIN_VALUE + */ + private static long checkOverflow(long t, long scale) { + if (t > Long.MAX_VALUE / scale) { + return Long.MAX_VALUE; + } + if (t < Long.MIN_VALUE / scale) { + return Long.MIN_VALUE; + } + return t * scale; + } + + /** + * 将毫秒转换为年月日时分秒 ,只拿时分秒 + * + * @author GaoHuanjie + */ + public static String getYearMonthDayHourMinuteSecond(long timeMillis) { + int timezone = 8; // 时区 + long totalSeconds = timeMillis / 1000; + totalSeconds += 60 * 60 * timezone; + int second = (int) (totalSeconds % 60);// 秒 + long totalMinutes = totalSeconds / 60; + int minute = (int) (totalMinutes % 60);// 分 + long totalHours = totalMinutes / 60; + int hour = (int) (totalHours % 24);// 时 + int totalDays = (int) (totalHours / 24); + int _year = 1970; + int year = _year + totalDays / 366; + int month = 1; + int day = 1; + int diffDays; + boolean leapYear; + while (true) { + int diff = (year - _year) * 365; + diff += (year - 1) / 4 - (_year - 1) / 4; + diff -= ((year - 1) / 100 - (_year - 1) / 100); + diff += (year - 1) / 400 - (_year - 1) / 400; + diffDays = totalDays - diff; + leapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); + if (!leapYear && diffDays < 365 || leapYear && diffDays < 366) { + break; + } else { + year++; + } + } + + int[] monthDays; + if (diffDays >= 59 && leapYear) { + monthDays = new int[]{-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + } else { + monthDays = new int[]{-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + } + for (int i = monthDays.length - 1; i >= 1; i--) { + if (diffDays >= monthDays[i]) { + month = i; + day = diffDays - monthDays[i] + 1; + break; + } + } + String hours; + String minutes; + String seconds; + if (hour < 10) { + hours = "0" + hour; + } else { + hours = "" + hour; + } + if (minute < 10) { + minutes = "0" + minute; + } else { + minutes = "" + minute; + } + if (second < 10) { + seconds = "0" + second; + } else { + seconds = "" + second; + } + +// return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second; + return hours + ":" + minutes + ":" + seconds; + } + + public static Date getTimesnight(int i) { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, i); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + /*获得指定天数的0点和24点*/ + public static Date getTimesnights(Date time) { + Calendar cal = Calendar.getInstance(); + cal.setTime(time); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + /** + * 将时间戳ms 转成星座 + * 白羊:0321~0420 天秤:0924~1023 + * 金牛:0421~0521 天蝎:1024~1122 + * 双子:0522~0621 射手:1123~1221 + * 巨蟹:0622~0722 摩羯:1222~0120 + * 狮子:0723~0823 水瓶:0121~0219 + * 处女:0824~0923 双鱼:0220~0320 + */ + public static String msToConstellation(long ms) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(ms); + int month = calendar.get(Calendar.MONTH) + 1; + int day = calendar.get(Calendar.DAY_OF_MONTH); + Log.e("mouse_debug", month + ", " + day); + String xingzuo = ""; + switch (month) { + case 1: + xingzuo = day < 21 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_01) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_02); + break; + case 2: + xingzuo = day < 20 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_03) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_04); + break; + case 3: + xingzuo = day < 21 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_05) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_06); + break; + case 4: + xingzuo = day < 21 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_07) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_08); + break; + case 5: + xingzuo = day < 22 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_09) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_010); + break; + case 6: + xingzuo = day < 22 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_011) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_012); + break; + case 7: + xingzuo = day < 23 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_013) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_014); + break; + case 8: + xingzuo = day < 24 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_015) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_016); + break; + case 9: + xingzuo = day < 24 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_017) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_018); + break; + case 10: + xingzuo = day < 24 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_019) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_020); + break; + case 11: + xingzuo = day < 23 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_021) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_022); + break; + case 12: + xingzuo = day < 22 ? ResUtil.getString(R.string.xchat_android_library_utils_timeutils_023) : ResUtil.getString(R.string.xchat_android_library_utils_timeutils_024); + break; + } + return xingzuo; + } + + /** + * 判断是不是 第二天 + * + * @param cacheTime 和 当前时间对比,判断现在是不是第二天了 + * @return + */ + public static boolean isTomorrow(long cacheTime) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + int currYear = calendar.get(Calendar.YEAR); + int currMonth = calendar.get(Calendar.MONTH); + int currDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + calendar.setTimeInMillis(cacheTime); + int cacheYear = calendar.get(Calendar.YEAR); + int cacheMonth = calendar.get(Calendar.MONTH); + int cacheDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + if (currYear > cacheYear) { + return true; + } + boolean equalYear = currYear == cacheYear; + if (equalYear && currMonth > cacheMonth) { + return true; + } + return equalYear && currMonth == cacheMonth && currDayOfMonth > cacheDayOfMonth; + } + + /** + * @param cacheTime 和 当前时间对比,判断现在是不是当天 + * @return - + */ + public static boolean isToday(long cacheTime) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + int currYear = calendar.get(Calendar.YEAR); + int currMonth = calendar.get(Calendar.MONTH); + int currDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + calendar.setTimeInMillis(cacheTime); + int cacheYear = calendar.get(Calendar.YEAR); + int cacheMonth = calendar.get(Calendar.MONTH); + int cacheDayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + return currYear == cacheYear && currMonth == cacheMonth && currDayOfMonth == cacheDayOfMonth; + } + + /** + * @param cacheTime 和 当前时间对比,是不是同一年 + * @return - + */ + public static boolean isSameYear(long cacheTime) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + int currYear = calendar.get(Calendar.YEAR); + calendar.setTimeInMillis(cacheTime); + int cacheYear = calendar.get(Calendar.YEAR); + return currYear == cacheYear; + } + + /** + * 获得系统时间 年、月、日、小时、分钟 + * + * @return HashMap + */ + public static int getTime24() { + SimpleDateFormat hh = new SimpleDateFormat("HH", Locale.CHINA); + SimpleDateFormat mm = new SimpleDateFormat("mm", Locale.CHINA); + hh.setTimeZone(TimeZone.getTimeZone("GMT+08")); // 获取指定时区的时间 + mm.setTimeZone(TimeZone.getTimeZone("GMT+08")); + Date date = new Date(); + String hour = hh.format(date); + String minute = mm.format(date); + return Integer.parseInt(hour + minute); + } + + public static boolean getTimeLimit(int start_hour, int end_hour) { + if (start_hour == end_hour) return false; + SimpleDateFormat hh = new SimpleDateFormat("HH", Locale.CHINA); + SimpleDateFormat mm = new SimpleDateFormat("mm", Locale.CHINA); + hh.setTimeZone(TimeZone.getTimeZone("GMT+08")); // 获取指定时区的时间 + mm.setTimeZone(TimeZone.getTimeZone("GMT+08")); + Date date = new Date(); + String hour = hh.format(date); + String minute = mm.format(date); + Log.i("stf", "--hour:minute-->" + hour + ":" + minute); + int currentTime = Integer.parseInt(hour + minute); + LogUtil.print("start" + start_hour); + LogUtil.print("end" + end_hour); + LogUtil.print("minuteOfDay" + currentTime); + if (currentTime >= start_hour && currentTime <= end_hour) { + LogUtil.print(ResUtil.getString(R.string.xchat_android_library_utils_timeutils_025) + hour + ":" + minute); + return true; + } else { + if (start_hour > end_hour) { + if (currentTime >= start_hour || currentTime <= end_hour) { + return true; + } + } + LogUtil.print(ResUtil.getString(R.string.xchat_android_library_utils_timeutils_026) + hour + ":" + minute); + return false; + } + } + + /** + * 获取指定年月的第一天 + * + * @param ym yyyy-MM + * @return + */ + public static String getFirstDayOfMonth(String ym) { + String arr[] = ym.split("-"); + + int year = Integer.parseInt(arr[0]); + int month = Integer.parseInt(arr[1]); + + Calendar cal = Calendar.getInstance(); + //设置年份 + cal.set(Calendar.YEAR, year); + + //设置月份 + cal.set(Calendar.MONTH, month - 1); + //获取某月最小天数 + int firstDay = cal.getMinimum(Calendar.DATE); + //设置日历中月份的最小天数 + cal.set(Calendar.DAY_OF_MONTH, firstDay); + //格式化日期 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + return sdf.format(cal.getTime()); + } + + /** + * 获取指定年月的最后一天 + * + * @param ym + * @return + */ + public static String getLastDayOfMonth(String ym) { + String arr[] = ym.split("-"); + + int year = Integer.parseInt(arr[0]); + int month = Integer.parseInt(arr[1]); + + Calendar cal = Calendar.getInstance(); + //设置年份 + cal.set(Calendar.YEAR, year); + //设置月份 + cal.set(Calendar.MONTH, month - 1); + //获取某月最大天数 + int lastDay = cal.getActualMaximum(Calendar.DATE); + //设置日历中月份的最大天数 + cal.set(Calendar.DAY_OF_MONTH, lastDay); + //格式化日期 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(cal.getTime()); + } + + public static String date2Str(Date d, String format) {// yyyy-MM-dd HH:mm:ss + if (d == null) { + return null; + } + if (format == null || format.length() == 0) { + format = TIME_FORMAT; + } + SimpleDateFormat sdf = new SimpleDateFormat(format); + return sdf.format(d); + } + + public static class YEARS { + public static long toMillis(long years) { + return checkOverflow(years, DAYS_OF_YEAR * HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE * MILLIS_OF_SECOND); + } + + public static long toSeconds(long years) { + return checkOverflow(years, DAYS_OF_YEAR * HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE); + } + + public static long toMinutes(long years) { + return checkOverflow(years, DAYS_OF_YEAR * HOURS_OF_DAY * MINUTES_OF_HOUR); + } + + public static long toHours(long years) { + return checkOverflow(years, DAYS_OF_YEAR * HOURS_OF_DAY); + } + + public static long toDays(long years) { + return checkOverflow(years, DAYS_OF_YEAR); + } + + public static long toMonths(long years) { + return checkOverflow(years, MONTHS_OF_YEAR); + } + } + + public static class MONTHS { + public static long toMillis(long months) { + return checkOverflow(months, DAYS_OF_MONTH * HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE * MILLIS_OF_SECOND); + } + + public static long toSeconds(long months) { + return checkOverflow(months, DAYS_OF_MONTH * HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE); + } + + public static long toMinutes(long months) { + return checkOverflow(months, DAYS_OF_MONTH * HOURS_OF_DAY * MINUTES_OF_HOUR); + } + + public static long toHours(long months) { + return checkOverflow(months, DAYS_OF_MONTH * HOURS_OF_DAY); + } + + public static long toDays(long months) { + return checkOverflow(months, DAYS_OF_MONTH); + } + + public static long toYears(long months) { + return months / MONTHS_OF_YEAR; + } + } + + public static class DAYS { + public static long toMillis(long days) { + return checkOverflow(days, HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE * MILLIS_OF_SECOND); + } + + public static long toSeconds(long days) { + return checkOverflow(days, HOURS_OF_DAY * MINUTES_OF_HOUR * SECONDS_OF_MINUTE); + } + + public static long toMinutes(long days) { + return checkOverflow(days, HOURS_OF_DAY * MINUTES_OF_HOUR); + } + + public static long toHours(long days) { + return checkOverflow(days, HOURS_OF_DAY); + } + + public static long toMonths(long days) { + return days / DAYS_OF_MONTH; + } + + public static long toYears(long days) { + return days / DAYS_OF_YEAR; + } + } + + public static class HOURS { + public static long toMillis(long hours) { + return checkOverflow(hours, MINUTES_OF_HOUR * SECONDS_OF_MINUTE * MILLIS_OF_SECOND); + } + + public static long toSeconds(long hours) { + return checkOverflow(hours, MINUTES_OF_HOUR * SECONDS_OF_MINUTE); + } + + public static long toMinutes(long hours) { + return checkOverflow(hours, MINUTES_OF_HOUR); + } + + public static long toDays(long hours) { + return hours / HOURS_OF_DAY; + } + + public static long toMonths(long hours) { + return toDays(hours) / DAYS_OF_MONTH; + } + + public static long toYears(long hours) { + return toDays(hours) / DAYS_OF_YEAR; + } + } + + public static class MINUTES { + public static long toMillis(long minutes) { + return checkOverflow(minutes, SECONDS_OF_MINUTE * MILLIS_OF_SECOND); + } + + public static long toSeconds(long minutes) { + return checkOverflow(minutes, SECONDS_OF_MINUTE); + } + + public static long toHours(long minutes) { + return minutes / MINUTES_OF_HOUR; + } + + public static long toDays(long minutes) { + return toHours(minutes) / HOURS_OF_DAY; + } + + public static long toMonths(long minutes) { + return toDays(minutes) / DAYS_OF_MONTH; + } + + public static long toYears(long minutes) { + return toDays(minutes) / DAYS_OF_YEAR; + } + } + + public static class SECONDS { + public static long toMillis(long seconds) { + return checkOverflow(seconds, MILLIS_OF_SECOND); + } + + public static long toMinutes(long seconds) { + return seconds / SECONDS_OF_MINUTE; + } + + public static long toHours(long seconds) { + return toMinutes(seconds) / MINUTES_OF_HOUR; + } + + public static long toDays(long seconds) { + return toHours(seconds) / HOURS_OF_DAY; + } + + public static long toMonths(long seconds) { + return toDays(seconds) / DAYS_OF_MONTH; + } + + public static long toYears(long seconds) { + return toDays(seconds) / DAYS_OF_YEAR; + } + } + + public static class MILLIS { + public static long toSeconds(long millis) { + return millis / MILLIS_OF_SECOND; + } + + public static long toMinutes(long millis) { + return toSeconds(millis) / SECONDS_OF_MINUTE; + } + + public static long toHours(long millis) { + return toMinutes(millis) / MINUTES_OF_HOUR; + } + + public static long toDays(long millis) { + return toHours(millis) / HOURS_OF_DAY; + } + + public static long toMonths(long millis) { + return toDays(millis) / DAYS_OF_MONTH; + } + + public static long toYears(long millis) { + return toDays(millis) / DAYS_OF_YEAR; + } + } + + +// public static boolean getTimeLimit() { +// SimpleDateFormat hh = new SimpleDateFormat("HH", Locale.CHINA); +// SimpleDateFormat mm = new SimpleDateFormat("mm", Locale.CHINA); +// hh.setTimeZone(TimeZone.getTimeZone("GMT+08")); // 获取指定时区的时间 +// mm.setTimeZone(TimeZone.getTimeZone("GMT+08")); +// Date date = new Date(); +// String hour = hh.format(date); +// String minute = mm.format(date); +// final int start = 8 * 60;// 起始时间 8:00的分钟数 +// final int end = 19 * 60;// 结束时间 19:00的分钟数 +// int minuteOfDay = Integer.parseInt(hour) * 60 + Integer.parseInt(minute); +// Log.i("stf", "--hour:minute-->" + hour + ":" + minute); +// if (minuteOfDay >= start && minuteOfDay <= end) { +// LogUtil.print(ResUtil.getString(R.string.xchat_android_library_utils_timeutils_027) + hour + ":" + minute); +// return true; +// } else { +// LogUtil.print( ResUtil.getString(R.string.xchat_android_library_utils_timeutils_028) + hour + ":" + minute); +// return false; +// } +// } +} diff --git a/library/src/main/java/com/chwl/library/utils/UIUtils.java b/library/src/main/java/com/chwl/library/utils/UIUtils.java new file mode 100644 index 0000000..87cbce6 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/UIUtils.java @@ -0,0 +1,196 @@ +package com.chwl.library.utils; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ListView; +import android.widget.PopupWindow; + +import com.chwl.library.utils.log.MLog; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; + +/** + * Created by lijun on 2015/1/15. + */ +public class UIUtils { + + public static View getViewByPosition(int pos, ListView listView) { + final int firstListItemPosition = listView.getFirstVisiblePosition(); + final int lastListItemPosition = listView.getLastVisiblePosition(); + + if (pos < firstListItemPosition || pos > lastListItemPosition ) { +// return listView.getAdapter().getView(pos, null, listView); + return null; + } else { + final int childIndex = pos - firstListItemPosition; + return listView.getChildAt(childIndex); + } + } + + public static void setPopupWindowTouchModal(PopupWindow popupWindow, + boolean touchModal) { + if (null == popupWindow) { + return; + } + Method method; + try { + + method = PopupWindow.class.getDeclaredMethod("setTouchModal", + boolean.class); + method.setAccessible(true); + method.invoke(popupWindow, touchModal); + + } + catch (Exception e) { + MLog.error("UIUtils", e); + } + } + + @TargetApi(17) + public static boolean checkActivityValid(Activity activity) { + try { + if (null == activity) { + return false; + } + + if (activity.isFinishing()) { + MLog.warn(activity, "activity is finishing"); + return false; + } + + if (Build.VERSION.SDK_INT >= 17 && activity.isDestroyed()) { + MLog.warn(activity, "activity is isDestroyed"); + return false; + } + } catch (Exception e) { + MLog.error(UIUtils.class, e); + } + + return true; + } + + public static boolean isTopActivity(Activity activity) { + if (null == activity) { + return false; + } + + ActivityManager manager = (ActivityManager) activity.getSystemService(Activity.ACTIVITY_SERVICE); + List runningTasks = manager.getRunningTasks(1); + String cmpNameTemp = null; + if (null != runningTasks) { + cmpNameTemp = (runningTasks.get(0).topActivity).getClassName(); + } + + if (null == cmpNameTemp) { + return false; + } + + return Objects.equals(cmpNameTemp, activity.getComponentName().getClassName()); + } + + public static boolean isTopActivity(Context context, String activity) { + if (null == activity) { + return false; + } + + ActivityManager manager = (ActivityManager) context.getSystemService(Activity.ACTIVITY_SERVICE); + List runningTasks = manager.getRunningTasks(1); + String cmpNameTemp = null; + if (null != runningTasks) { + cmpNameTemp = (runningTasks.get(0).topActivity).getClassName(); + } + + if (null == cmpNameTemp) { + return false; + } + + return Objects.equals(cmpNameTemp, activity); + } + + public static Activity getActivityByContext(Context context){ + while(context instanceof ContextWrapper){ + if(context instanceof Activity){ + return (Activity) context; + } + context = ((ContextWrapper) context).getBaseContext(); + } + return null; + } + + public static void setOverflowShowingAlways(Context context) { + try { + ViewConfiguration config = ViewConfiguration.get(context); + Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); + menuKeyField.setAccessible(true); + menuKeyField.setBoolean(config, false); + } catch (Exception e) { + MLog.error(UIUtils.class, e); + } + } + + @TargetApi(19) + public static void enableTranslucentStatus(Activity activity) { + setTranslucentStatus(activity, true); + } + + @TargetApi(19) + public static void disableTranslucentStatus(Activity activity) { + setTranslucentStatus(activity, false); + } + + private static void setTranslucentStatus(Activity activity, boolean enable) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + + if (null == activity) { + return; + } + + Window window = activity.getWindow(); + if (enable) { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + @TargetApi(19) + public static void enableTranslucentNavigation(Activity activity) { + setTranslucentNavigation(activity, true); + } + + @TargetApi(19) + public static void disableTranslucentNavigation(Activity activity) { + setTranslucentNavigation(activity, false); + } + + private static void setTranslucentNavigation(Activity activity, boolean enable) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + + if (null == activity) { + return; + } + + Window window = activity.getWindow(); + if (enable) { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/UUIDUtil.java b/library/src/main/java/com/chwl/library/utils/UUIDUtil.java new file mode 100644 index 0000000..dbc53f0 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/UUIDUtil.java @@ -0,0 +1,13 @@ +package com.chwl.library.utils; + +import java.util.UUID; + +/** + * Created by lijun on 2014/12/3. + */ +public class UUIDUtil { + + public static String getUUID() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/UriUtil.java b/library/src/main/java/com/chwl/library/utils/UriUtil.java new file mode 100644 index 0000000..e88942f --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/UriUtil.java @@ -0,0 +1,86 @@ +package com.chwl.library.utils; + +import android.net.Uri; + +/** + * Created by lijun on 2015/6/18. + */ +public class UriUtil { + + public static final String SCHEME_HTTP_TAG = "http"; + public static final String SCHEME_HTTPS_TAG = "https"; + public static final String SCHEME_FILE_TAG = "file"; + + /** + * NOTICE you may need URLUtil.isHttpUrl(String url) + * @param url + * @return + */ + public static boolean isHttpUrl(String url) { + if (StringUtils.isEmpty(url)) { + return false; + } + + return isHttpUri(Uri.parse(url)); + } + + public static boolean isHttpUri(Uri uri) { + if (null == uri) { + return false; + } + + String scheme = uri.getScheme(); + if (StringUtils.isEmpty(scheme)) { + return false; + } + return SCHEME_HTTP_TAG.equals(scheme) || SCHEME_HTTPS_TAG.equals(scheme); + } + + public static boolean isFileUri(Uri uri) { + if (null == uri) { + return false; + } + String scheme = uri.getScheme(); + if (StringUtils.isEmpty(scheme)) { + return false; + } + return SCHEME_FILE_TAG.equals(scheme); + } + + public static Uri generateHttpUri(String url) { + if (isHttpUrl(url)) { + return Uri.parse(url); + } else if (StringUtils.isNotEmpty(url)) { + return Uri.parse(SCHEME_HTTP_TAG + "://" + url); + } + return null; + } + + /** + * 从http url获取参数 + * @param url + * @param key + * @return + */ + public static String getParamFromUrl(String url, String key) { + if (StringUtils.isNotEmpty(url) && StringUtils.isNotEmpty(key)) { + int queryIndex = url.indexOf('?'); + if (queryIndex > 0) { + String[] paramMap = url.substring(queryIndex + 1).split("&"); + if (paramMap != null) { + for (String map : paramMap) { + String[] paramKv = map.split("="); + if (paramKv.length == 2 && key.equals(paramKv[0])) { + return paramKv[1]; + } + } + } + } + } + return null; + } + + public static void main(String[] args) { + System.out.println(getParamFromUrl("adfasdf?777=99&ss=11", "77")); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/Validate.java b/library/src/main/java/com/chwl/library/utils/Validate.java new file mode 100644 index 0000000..9b2cc85 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/Validate.java @@ -0,0 +1,1226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.chwl.library.utils; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; + +/** + *

This class assists in validating arguments. The validation methods are + * based along the following principles: + *

    + *
  • An invalid {@code null} argument causes a {@link NullPointerException}.
  • + *
  • A non-{@code null} argument causes an {@link IllegalArgumentException}.
  • + *
  • An invalid index into an array/collection/map/string causes an {@link IndexOutOfBoundsException}.
  • + *
+ *

+ *

All exceptions messages are + * format strings + * as defined by the Java platform. For example:

+ *

+ *

+ * Validate.isTrue(i > 0, "The value must be greater than zero: %d", i);
+ * Validate.notNull(surname, "The surname must not be %s", null);
+ * 
+ *

+ *

#ThreadSafe#

+ * + * @version $Id: Validate.java 1583482 2014-03-31 22:54:57Z niallp $ + * @see String#format(String, Object...) + * @since 2.0 + */ +public class Validate { + + private static final String DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE = + "The value %s is not in the specified exclusive range of %s to %s"; + private static final String DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE = + "The value %s is not in the specified inclusive range of %s to %s"; + private static final String DEFAULT_MATCHES_PATTERN_EX = "The string %s does not match the pattern %s"; + private static final String DEFAULT_IS_NULL_EX_MESSAGE = "The validated object is null"; + private static final String DEFAULT_IS_TRUE_EX_MESSAGE = "The validated expression is false"; + private static final String DEFAULT_NO_NULL_ELEMENTS_ARRAY_EX_MESSAGE = + "The validated array contains null element at index: %d"; + private static final String DEFAULT_NO_NULL_ELEMENTS_COLLECTION_EX_MESSAGE = + "The validated collection contains null element at index: %d"; + private static final String DEFAULT_NOT_BLANK_EX_MESSAGE = "The validated character sequence is blank"; + private static final String DEFAULT_NOT_EMPTY_ARRAY_EX_MESSAGE = "The validated array is empty"; + private static final String DEFAULT_NOT_EMPTY_CHAR_SEQUENCE_EX_MESSAGE = + "The validated character sequence is empty"; + private static final String DEFAULT_NOT_EMPTY_COLLECTION_EX_MESSAGE = "The validated collection is empty"; + private static final String DEFAULT_NOT_EMPTY_MAP_EX_MESSAGE = "The validated map is empty"; + private static final String DEFAULT_VALID_INDEX_ARRAY_EX_MESSAGE = "The validated array index is invalid: %d"; + private static final String DEFAULT_VALID_INDEX_CHAR_SEQUENCE_EX_MESSAGE = + "The validated character sequence index is invalid: %d"; + private static final String DEFAULT_VALID_INDEX_COLLECTION_EX_MESSAGE = + "The validated collection index is invalid: %d"; + private static final String DEFAULT_VALID_STATE_EX_MESSAGE = "The validated state is false"; + private static final String DEFAULT_IS_ASSIGNABLE_EX_MESSAGE = "Cannot assign a %s to a %s"; + private static final String DEFAULT_IS_INSTANCE_OF_EX_MESSAGE = "Expected type: %s, actual: %s"; + + /** + * Constructor. This class should not normally be instantiated. + */ + public Validate() { + super(); + } + + // isTrue + //--------------------------------------------------------------------------------- + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

Validate.isTrue(i > 0.0, "The value must be greater than zero: %d", i);
+ *

+ *

For performance reasons, the long value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, double) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(final boolean expression, final String message, final long value) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, Long.valueOf(value))); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

Validate.isTrue(d > 0.0, "The value must be greater than zero: %s", d);
+ *

+ *

For performance reasons, the double value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(final boolean expression, final String message, final double value) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, Double.valueOf(value))); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

+     * Validate.isTrue(i >= min && i <= max, "The value must be between %d and %d", min, max);
+     * Validate.isTrue(myObject.isOk(), "The object is not okay");
+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, double) + */ + public static void isTrue(final boolean expression, final String message, final Object... values) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception. This method is useful when validating according + * to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

+     * Validate.isTrue(i > 0);
+     * Validate.isTrue(myObject.isOk());
+ * + *

The message of the exception is "The validated expression is + * false".

+ * + * @param expression the boolean expression to check + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, double) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(final boolean expression) { + if (expression == false) { + throw new IllegalArgumentException(DEFAULT_IS_TRUE_EX_MESSAGE); + } + } + + // notNull + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument is not {@code null}; + * otherwise throwing an exception. + *

+ *

Validate.notNull(myObject, "The object must not be null");
+ *

+ *

The message of the exception is "The validated object is + * null".

+ * + * @param the object type + * @param object the object to check + * @return the validated object (never {@code null} for method chaining) + * @throws NullPointerException if the object is {@code null} + * @see #notNull(Object, String, Object...) + */ + public static T notNull(final T object) { + return notNull(object, DEFAULT_IS_NULL_EX_MESSAGE); + } + + /** + *

Validate that the specified argument is not {@code null}; + * otherwise throwing an exception with the specified message. + *

+ *

Validate.notNull(myObject, "The object must not be null");
+ * + * @param the object type + * @param object the object to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message + * @return the validated object (never {@code null} for method chaining) + * @throws NullPointerException if the object is {@code null} + * @see #notNull(Object) + */ + public static T notNull(final T object, final String message, final Object... values) { + if (object == null) { + throw new NullPointerException(String.format(message, values)); + } + return object; + } + + // notEmpty array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither {@code null} + * nor a length of zero (no elements); otherwise throwing an exception + * with the specified message. + *

+ *

Validate.notEmpty(myArray, "The array must not be empty");
+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array is empty + * @see #notEmpty(Object[]) + */ + public static T[] notEmpty(final T[] array, final String message, final Object... values) { + if (array == null) { + throw new NullPointerException(String.format(message, values)); + } + if (array.length == 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + return array; + } + + /** + *

Validate that the specified argument array is neither {@code null} + * nor a length of zero (no elements); otherwise throwing an exception. + *

+ *

Validate.notEmpty(myArray);
+ *

+ *

The message in the exception is "The validated array is + * empty". + * + * @param the array type + * @param array the array to check, validated not null by this method + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array is empty + * @see #notEmpty(Object[], String, Object...) + */ + public static T[] notEmpty(final T[] array) { + return notEmpty(array, DEFAULT_NOT_EMPTY_ARRAY_EX_MESSAGE); + } + + // notEmpty collection + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument collection is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + *

+ *

Validate.notEmpty(myCollection, "The collection must not be empty");
+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated collection (never {@code null} method for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IllegalArgumentException if the collection is empty + * @see #notEmpty(Object[]) + */ + public static > T notEmpty(final T collection, final String message, final Object... values) { + if (collection == null) { + throw new NullPointerException(String.format(message, values)); + } + if (collection.isEmpty()) { + throw new IllegalArgumentException(String.format(message, values)); + } + return collection; + } + + /** + *

Validate that the specified argument collection is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception. + *

+ *

Validate.notEmpty(myCollection);
+ *

+ *

The message in the exception is "The validated collection is + * empty".

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @return the validated collection (never {@code null} method for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IllegalArgumentException if the collection is empty + * @see #notEmpty(Collection, String, Object...) + */ + public static > T notEmpty(final T collection) { + return notEmpty(collection, DEFAULT_NOT_EMPTY_COLLECTION_EX_MESSAGE); + } + + // notEmpty map + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument map is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + *

+ *

Validate.notEmpty(myMap, "The map must not be empty");
+ * + * @param the map type + * @param map the map to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated map (never {@code null} method for chaining) + * @throws NullPointerException if the map is {@code null} + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(Object[]) + */ + public static > T notEmpty(final T map, final String message, final Object... values) { + if (map == null) { + throw new NullPointerException(String.format(message, values)); + } + if (map.isEmpty()) { + throw new IllegalArgumentException(String.format(message, values)); + } + return map; + } + + /** + *

Validate that the specified argument map is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception. + *

+ *

Validate.notEmpty(myMap);
+ *

+ *

The message in the exception is "The validated map is + * empty".

+ * + * @param the map type + * @param map the map to check, validated not null by this method + * @return the validated map (never {@code null} method for chaining) + * @throws NullPointerException if the map is {@code null} + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(Map, String, Object...) + */ + public static > T notEmpty(final T map) { + return notEmpty(map, DEFAULT_NOT_EMPTY_MAP_EX_MESSAGE); + } + + // notEmpty string + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence is + * neither {@code null} nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + *

+ *

Validate.notEmpty(myString, "The string must not be empty");
+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is empty + * @see #notEmpty(CharSequence) + */ + public static T notEmpty(final T chars, final String message, final Object... values) { + if (chars == null) { + throw new NullPointerException(String.format(message, values)); + } + if (chars.length() == 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + return chars; + } + + /** + *

Validate that the specified argument character sequence is + * neither {@code null} nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + *

+ *

Validate.notEmpty(myString);
+ *

+ *

The message in the exception is "The validated + * character sequence is empty".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is empty + * @see #notEmpty(CharSequence, String, Object...) + */ + public static T notEmpty(final T chars) { + return notEmpty(chars, DEFAULT_NOT_EMPTY_CHAR_SEQUENCE_EX_MESSAGE); + } + + // notBlank string + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence is + * neither {@code null}, a length of zero (no characters), empty + * nor whitespace; otherwise throwing an exception with the specified + * message. + *

+ *

Validate.notBlank(myString, "The string must not be blank");
+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is blank + * @see #notBlank(CharSequence) + * @since 3.0 + */ + public static T notBlank(final T chars, final String message, final Object... values) { + if (chars == null) { + throw new NullPointerException(String.format(message, values)); + } + if (StringUtils.isBlank(chars)) { + throw new IllegalArgumentException(String.format(message, values)); + } + return chars; + } + + /** + *

Validate that the specified argument character sequence is + * neither {@code null}, a length of zero (no characters), empty + * nor whitespace; otherwise throwing an exception. + *

+ *

Validate.notBlank(myString);
+ *

+ *

The message in the exception is "The validated character + * sequence is blank".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is blank + * @see #notBlank(CharSequence, String, Object...) + * @since 3.0 + */ + public static T notBlank(final T chars) { + return notBlank(chars, DEFAULT_NOT_BLANK_EX_MESSAGE); + } + + // noNullElements array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception with the specified message. + *

+ *

Validate.noNullElements(myArray, "The array contain null at position %d");
+ *

+ *

If the array is {@code null}, then the message in the exception + * is "The validated object is null".

+ *

+ *

If the array has a {@code null} element, then the iteration + * index of the invalid element is appended to the {@code values} + * argument.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Object[]) + */ + public static T[] noNullElements(final T[] array, final String message, final Object... values) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + final Object[] values2 = ArrayUtils.add(values, Integer.valueOf(i)); + throw new IllegalArgumentException(String.format(message, values2)); + } + } + return array; + } + + /** + *

Validate that the specified argument array is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception.

+ *

+ *

Validate.noNullElements(myArray);
+ *

+ *

If the array is {@code null}, then the message in the exception + * is "The validated object is null".

+ *

+ *

If the array has a {@code null} element, then the message in the + * exception is "The validated array contains null element at index: + * " followed by the index.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Object[], String, Object...) + */ + public static T[] noNullElements(final T[] array) { + return noNullElements(array, DEFAULT_NO_NULL_ELEMENTS_ARRAY_EX_MESSAGE); + } + + // noNullElements iterable + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument iterable is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception with the specified message. + *

+ *

Validate.noNullElements(myCollection, "The collection contains null at position %d");
+ *

+ *

If the iterable is {@code null}, then the message in the exception + * is "The validated object is null".

+ *

+ *

If the iterable has a {@code null} element, then the iteration + * index of the invalid element is appended to the {@code values} + * argument.

+ * + * @param the iterable type + * @param iterable the iterable to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated iterable (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Iterable) + */ + public static > T noNullElements(final T iterable, final String message, final Object... values) { + Validate.notNull(iterable); + int i = 0; + for (final Iterator it = iterable.iterator(); it.hasNext(); i++) { + if (it.next() == null) { + final Object[] values2 = ArrayUtils.addAll(values, Integer.valueOf(i)); + throw new IllegalArgumentException(String.format(message, values2)); + } + } + return iterable; + } + + /** + *

Validate that the specified argument iterable is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception. + *

+ *

Validate.noNullElements(myCollection);
+ *

+ *

If the iterable is {@code null}, then the message in the exception + * is "The validated object is null".

+ *

+ *

If the array has a {@code null} element, then the message in the + * exception is "The validated iterable contains null element at index: + * " followed by the index.

+ * + * @param the iterable type + * @param iterable the iterable to check, validated not null by this method + * @return the validated iterable (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Iterable, String, Object...) + */ + public static > T noNullElements(final T iterable) { + return noNullElements(iterable, DEFAULT_NO_NULL_ELEMENTS_COLLECTION_EX_MESSAGE); + } + + // validIndex array + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * array; otherwise throwing an exception with the specified message.

+ *

+ *

Validate.validIndex(myArray, 2, "The array index is invalid: ");
+ *

+ *

If the array is {@code null}, then the message of the exception + * is "The validated object is null".

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} for method chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Object[], int) + * @since 3.0 + */ + public static T[] validIndex(final T[] array, final int index, final String message, final Object... values) { + Validate.notNull(array); + if (index < 0 || index >= array.length) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return array; + } + + /** + *

Validates that the index is within the bounds of the argument + * array; otherwise throwing an exception.

+ *

+ *

Validate.validIndex(myArray, 2);
+ *

+ *

If the array is {@code null}, then the message of the exception + * is "The validated object is null".

+ *

+ *

If the index is invalid, then the message of the exception is + * "The validated array index is invalid: " followed by the + * index.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param index the index to check + * @return the validated array (never {@code null} for method chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Object[], int, String, Object...) + * @since 3.0 + */ + public static T[] validIndex(final T[] array, final int index) { + return validIndex(array, index, DEFAULT_VALID_INDEX_ARRAY_EX_MESSAGE, Integer.valueOf(index)); + } + + // validIndex collection + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * collection; otherwise throwing an exception with the specified message.

+ *

+ *

Validate.validIndex(myCollection, 2, "The collection index is invalid: ");
+ *

+ *

If the collection is {@code null}, then the message of the + * exception is "The validated object is null".

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated collection (never {@code null} for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Collection, int) + * @since 3.0 + */ + public static > T validIndex(final T collection, final int index, final String message, final Object... values) { + Validate.notNull(collection); + if (index < 0 || index >= collection.size()) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return collection; + } + + /** + *

Validates that the index is within the bounds of the argument + * collection; otherwise throwing an exception.

+ *

+ *

Validate.validIndex(myCollection, 2);
+ *

+ *

If the index is invalid, then the message of the exception + * is "The validated collection index is invalid: " + * followed by the index.

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param index the index to check + * @return the validated collection (never {@code null} for method chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Collection, int, String, Object...) + * @since 3.0 + */ + public static > T validIndex(final T collection, final int index) { + return validIndex(collection, index, DEFAULT_VALID_INDEX_COLLECTION_EX_MESSAGE, Integer.valueOf(index)); + } + + // validIndex string + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * character sequence; otherwise throwing an exception with the + * specified message.

+ *

+ *

Validate.validIndex(myStr, 2, "The string index is invalid: ");
+ *

+ *

If the character sequence is {@code null}, then the message + * of the exception is "The validated object is null".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} for method chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(CharSequence, int) + * @since 3.0 + */ + public static T validIndex(final T chars, final int index, final String message, final Object... values) { + Validate.notNull(chars); + if (index < 0 || index >= chars.length()) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return chars; + } + + /** + *

Validates that the index is within the bounds of the argument + * character sequence; otherwise throwing an exception.

+ *

+ *

Validate.validIndex(myStr, 2);
+ *

+ *

If the character sequence is {@code null}, then the message + * of the exception is "The validated object is + * null".

+ *

+ *

If the index is invalid, then the message of the exception + * is "The validated character sequence index is invalid: " + * followed by the index.

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param index the index to check + * @return the validated character sequence (never {@code null} for method chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(CharSequence, int, String, Object...) + * @since 3.0 + */ + public static T validIndex(final T chars, final int index) { + return validIndex(chars, index, DEFAULT_VALID_INDEX_CHAR_SEQUENCE_EX_MESSAGE, Integer.valueOf(index)); + } + + // validState + //--------------------------------------------------------------------------------- + + /** + *

Validate that the stateful condition is {@code true}; otherwise + * throwing an exception. This method is useful when validating according + * to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

+     * Validate.validState(field > 0);
+     * Validate.validState(this.isOk());
+ * + *

The message of the exception is "The validated state is + * false".

+ * + * @param expression the boolean expression to check + * @throws IllegalStateException if expression is {@code false} + * @see #validState(boolean, String, Object...) + * @since 3.0 + */ + public static void validState(final boolean expression) { + if (expression == false) { + throw new IllegalStateException(DEFAULT_VALID_STATE_EX_MESSAGE); + } + } + + /** + *

Validate that the stateful condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ *

+ *

Validate.validState(this.isOk(), "The state is not OK: %s", myObject);
+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalStateException if expression is {@code false} + * @see #validState(boolean) + * @since 3.0 + */ + public static void validState(final boolean expression, final String message, final Object... values) { + if (expression == false) { + throw new IllegalStateException(String.format(message, values)); + } + } + + // matchesPattern + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence matches the specified regular + * expression pattern; otherwise throwing an exception.

+ *

+ *

Validate.matchesPattern("hi", "[a-z]*");
+ *

+ *

The syntax of the pattern is the one used in the {@link Pattern} class.

+ * + * @param input the character sequence to validate, not null + * @param pattern the regular expression pattern, not null + * @throws IllegalArgumentException if the character sequence does not match the pattern + * @see #matchesPattern(CharSequence, String, String, Object...) + * @since 3.0 + */ + public static void matchesPattern(final CharSequence input, final String pattern) { + // TODO when breaking BC, consider returning input + if (Pattern.matches(pattern, input) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_MATCHES_PATTERN_EX, input, pattern)); + } + } + + /** + *

Validate that the specified argument character sequence matches the specified regular + * expression pattern; otherwise throwing an exception with the specified message.

+ *

+ *

Validate.matchesPattern("hi", "[a-z]*", "%s does not match %s", "hi" "[a-z]*");
+ *

+ *

The syntax of the pattern is the one used in the {@link Pattern} class.

+ * + * @param input the character sequence to validate, not null + * @param pattern the regular expression pattern, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the character sequence does not match the pattern + * @see #matchesPattern(CharSequence, String) + * @since 3.0 + */ + public static void matchesPattern(final CharSequence input, final String pattern, final String message, final Object... values) { + // TODO when breaking BC, consider returning input + if (Pattern.matches(pattern, input) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // inclusiveBetween + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument object fall between the two + * inclusive values specified; otherwise, throws an exception.

+ *

+ *

Validate.inclusiveBetween(0, 2, 1);
+ * + * @param the type of the argument object + * @param start the inclusive start value, not null + * @param end the inclusive end value, not null + * @param value the object to validate, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @see #inclusiveBetween(Object, Object, Comparable, String, Object...) + * @since 3.0 + */ + public static void inclusiveBetween(final T start, final T end, final Comparable value) { + // TODO when breaking BC, consider returning value + if (value.compareTo(start) < 0 || value.compareTo(end) > 0) { + throw new IllegalArgumentException(String.format(DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + *

Validate that the specified argument object fall between the two + * inclusive values specified; otherwise, throws an exception with the + * specified message.

+ *

+ *

Validate.inclusiveBetween(0, 2, 1, "Not in boundaries");
+ * + * @param the type of the argument object + * @param start the inclusive start value, not null + * @param end the inclusive end value, not null + * @param value the object to validate, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the value falls outside the boundaries + * @see #inclusiveBetween(Object, Object, Comparable) + * @since 3.0 + */ + public static void inclusiveBetween(final T start, final T end, final Comparable value, final String message, final Object... values) { + // TODO when breaking BC, consider returning value + if (value.compareTo(start) < 0 || value.compareTo(end) > 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * inclusive values specified; otherwise, throws an exception. + *

+ *

Validate.inclusiveBetween(0, 2, 1);
+ * + * @param start the inclusive start value + * @param end the inclusive end value + * @param value the value to validate + * @throws IllegalArgumentException if the value falls outside the boundaries (inclusive) + * @since 3.3 + */ + @SuppressWarnings("boxing") + public static void inclusiveBetween(long start, long end, long value) { + // TODO when breaking BC, consider returning value + if (value < start || value > end) { + throw new IllegalArgumentException(String.format(DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * inclusive values specified; otherwise, throws an exception with the + * specified message. + *

+ *

Validate.inclusiveBetween(0, 2, 1, "Not in range");
+ * + * @param start the inclusive start value + * @param end the inclusive end value + * @param value the value to validate + * @param message the exception message if invalid, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @since 3.3 + */ + public static void inclusiveBetween(long start, long end, long value, String message) { + // TODO when breaking BC, consider returning value + if (value < start || value > end) { + throw new IllegalArgumentException(String.format(message)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * inclusive values specified; otherwise, throws an exception. + *

+ *

Validate.inclusiveBetween(0.1, 2.1, 1.1);
+ * + * @param start the inclusive start value + * @param end the inclusive end value + * @param value the value to validate + * @throws IllegalArgumentException if the value falls outside the boundaries (inclusive) + * @since 3.3 + */ + @SuppressWarnings("boxing") + public static void inclusiveBetween(double start, double end, double value) { + // TODO when breaking BC, consider returning value + if (value < start || value > end) { + throw new IllegalArgumentException(String.format(DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * inclusive values specified; otherwise, throws an exception with the + * specified message. + *

+ *

Validate.inclusiveBetween(0.1, 2.1, 1.1, "Not in range");
+ * + * @param start the inclusive start value + * @param end the inclusive end value + * @param value the value to validate + * @param message the exception message if invalid, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @since 3.3 + */ + public static void inclusiveBetween(double start, double end, double value, String message) { + // TODO when breaking BC, consider returning value + if (value < start || value > end) { + throw new IllegalArgumentException(String.format(message)); + } + } + + // exclusiveBetween + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument object fall between the two + * exclusive values specified; otherwise, throws an exception.

+ *

+ *

Validate.exclusiveBetween(0, 2, 1);
+ * + * @param the type of the argument object + * @param start the exclusive start value, not null + * @param end the exclusive end value, not null + * @param value the object to validate, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @see #exclusiveBetween(Object, Object, Comparable, String, Object...) + * @since 3.0 + */ + public static void exclusiveBetween(final T start, final T end, final Comparable value) { + // TODO when breaking BC, consider returning value + if (value.compareTo(start) <= 0 || value.compareTo(end) >= 0) { + throw new IllegalArgumentException(String.format(DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + *

Validate that the specified argument object fall between the two + * exclusive values specified; otherwise, throws an exception with the + * specified message.

+ *

+ *

Validate.exclusiveBetween(0, 2, 1, "Not in boundaries");
+ * + * @param the type of the argument object + * @param start the exclusive start value, not null + * @param end the exclusive end value, not null + * @param value the object to validate, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the value falls outside the boundaries + * @see #exclusiveBetween(Object, Object, Comparable) + * @since 3.0 + */ + public static void exclusiveBetween(final T start, final T end, final Comparable value, final String message, final Object... values) { + // TODO when breaking BC, consider returning value + if (value.compareTo(start) <= 0 || value.compareTo(end) >= 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * exclusive values specified; otherwise, throws an exception. + *

+ *

Validate.exclusiveBetween(0, 2, 1);
+ * + * @param start the exclusive start value + * @param end the exclusive end value + * @param value the value to validate + * @throws IllegalArgumentException if the value falls out of the boundaries + * @since 3.3 + */ + @SuppressWarnings("boxing") + public static void exclusiveBetween(long start, long end, long value) { + // TODO when breaking BC, consider returning value + if (value <= start || value >= end) { + throw new IllegalArgumentException(String.format(DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * exclusive values specified; otherwise, throws an exception with the + * specified message. + *

+ *

Validate.exclusiveBetween(0, 2, 1, "Not in range");
+ * + * @param start the exclusive start value + * @param end the exclusive end value + * @param value the value to validate + * @param message the exception message if invalid, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @since 3.3 + */ + public static void exclusiveBetween(long start, long end, long value, String message) { + // TODO when breaking BC, consider returning value + if (value <= start || value >= end) { + throw new IllegalArgumentException(String.format(message)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * exclusive values specified; otherwise, throws an exception. + *

+ *

Validate.exclusiveBetween(0.1, 2.1, 1.1);
+ * + * @param start the exclusive start value + * @param end the exclusive end value + * @param value the value to validate + * @throws IllegalArgumentException if the value falls out of the boundaries + * @since 3.3 + */ + @SuppressWarnings("boxing") + public static void exclusiveBetween(double start, double end, double value) { + // TODO when breaking BC, consider returning value + if (value <= start || value >= end) { + throw new IllegalArgumentException(String.format(DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + * Validate that the specified primitive value falls between the two + * exclusive values specified; otherwise, throws an exception with the + * specified message. + *

+ *

Validate.exclusiveBetween(0.1, 2.1, 1.1, "Not in range");
+ * + * @param start the exclusive start value + * @param end the exclusive end value + * @param value the value to validate + * @param message the exception message if invalid, not null + * @throws IllegalArgumentException if the value falls outside the boundaries + * @since 3.3 + */ + public static void exclusiveBetween(double start, double end, double value, String message) { + // TODO when breaking BC, consider returning value + if (value <= start || value >= end) { + throw new IllegalArgumentException(String.format(message)); + } + } + + // isInstanceOf + //--------------------------------------------------------------------------------- + + /** + * Validates that the argument is an instance of the specified class, if not throws an exception. + *

+ *

This method is useful when validating according to an arbitrary class

+ *

+ *

Validate.isInstanceOf(OkClass.class, object);
+ *

+ *

The message of the exception is "Expected type: {type}, actual: {obj_type}"

+ * + * @param type the class the object must be validated against, not null + * @param obj the object to check, null throws an exception + * @throws IllegalArgumentException if argument is not of specified class + * @see #isInstanceOf(Class, Object, String, Object...) + * @since 3.0 + */ + public static void isInstanceOf(final Class type, final Object obj) { + // TODO when breaking BC, consider returning obj + if (type.isInstance(obj) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_IS_INSTANCE_OF_EX_MESSAGE, type.getName(), + obj == null ? "null" : obj.getClass().getName())); + } + } + + /** + *

Validate that the argument is an instance of the specified class; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary class

+ *

+ *

Validate.isInstanceOf(OkClass.classs, object, "Wrong class, object is of class %s",
+     *   object.getClass().getName());
+ * + * @param type the class the object must be validated against, not null + * @param obj the object to check, null throws an exception + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if argument is not of specified class + * @see #isInstanceOf(Class, Object) + * @since 3.0 + */ + public static void isInstanceOf(final Class type, final Object obj, final String message, final Object... values) { + // TODO when breaking BC, consider returning obj + if (type.isInstance(obj) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // isAssignableFrom + //--------------------------------------------------------------------------------- + + /** + * Validates that the argument can be converted to the specified class, if not, throws an exception. + *

+ *

This method is useful when validating that there will be no casting errors.

+ *

+ *

Validate.isAssignableFrom(SuperClass.class, object.getClass());
+ *

+ *

The message format of the exception is "Cannot assign {type} to {superType}"

+ * + * @param superType the class the class must be validated against, not null + * @param type the class to check, not null + * @throws IllegalArgumentException if type argument is not assignable to the specified superType + * @see #isAssignableFrom(Class, Class, String, Object...) + * @since 3.0 + */ + public static void isAssignableFrom(final Class superType, final Class type) { + // TODO when breaking BC, consider returning type + if (superType.isAssignableFrom(type) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_IS_ASSIGNABLE_EX_MESSAGE, type == null ? "null" : type.getName(), + superType.getName())); + } + } + + /** + * Validates that the argument can be converted to the specified class, if not throws an exception. + *

+ *

This method is useful when validating if there will be no casting errors.

+ *

+ *

Validate.isAssignableFrom(SuperClass.class, object.getClass());
+ *

+ *

The message of the exception is "The validated object can not be converted to the" + * followed by the name of the class and "class"

+ * + * @param superType the class the class must be validated against, not null + * @param type the class to check, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if argument can not be converted to the specified class + * @see #isAssignableFrom(Class, Class) + */ + public static void isAssignableFrom(final Class superType, final Class type, final String message, final Object... values) { + // TODO when breaking BC, consider returning type + if (superType.isAssignableFrom(type) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/VersionUtil.java b/library/src/main/java/com/chwl/library/utils/VersionUtil.java new file mode 100644 index 0000000..73a3030 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/VersionUtil.java @@ -0,0 +1,231 @@ +package com.chwl.library.utils; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; + +import com.chwl.library.R; +import com.chwl.library.utils.config.BasicConfig; + +public class VersionUtil { + static int sLocalVer[] = null; + static String sLocalName = null; + private static final String SNAPSHOT = "-SNAPSHOT"; + + private static final String DOT = "."; + public static Ver getVerFromStr(String version) { + String normalVer = version; + if (version != null && version.contains(SNAPSHOT)) { + normalVer = version.replace(SNAPSHOT, ""); + } + Ver ver = null; + if (normalVer != null && normalVer.matches("\\d{1,}.\\d{1,}.\\d{1,}")) { + ver = new Ver(); + int dotPos = normalVer.indexOf(DOT); + int prevPos = 0; + ver.mMajor = Integer.valueOf(normalVer.substring(prevPos, dotPos)); + prevPos = dotPos + 1; + dotPos = normalVer.indexOf(DOT, prevPos); + ver.mMinor = Integer.valueOf(normalVer.substring(prevPos, dotPos)); + prevPos = dotPos + 1; + ver.mBuild = Integer.valueOf(normalVer.substring(prevPos)); + ver.isSnapshot = version.contains(SNAPSHOT); +// return ver; + } + return ver; + } + + public static Ver getLocalVer(Context c) { + Ver v = new Ver(); + int ver[] = getLocal(c); + if(ver != null && ver.length > 0) { + v.mMajor = ver[0]; + if(ver.length > 1) { + v.mMinor = ver[1]; + if(ver.length > 2) { + v.mBuild = ver[2]; + if(ver.length > 3) + v.isSnapshot = ver[3] == 1 ? true : false; + } + } + } + return v; + } + + public static String getLocalName(Context c){ + if( sLocalName != null ){ + return sLocalName; + } + + try { + loadLoaclVer(c); + } catch (Exception e) { + sLocalVer = new int[4]; + sLocalVer[0] = 0; + sLocalVer[1] = 0; + sLocalVer[2] = 0; + sLocalVer[3] = 0; + } + + return sLocalName; + } + + public static int[] getLocal(Context c){ + if( sLocalVer != null ){ + return sLocalVer.clone(); + } + try { + loadLoaclVer(c); + } catch (Exception e) { + sLocalVer = new int[4]; + sLocalVer[0] = 0; + sLocalVer[1] = 0; + sLocalVer[2] = 0; + sLocalVer[3] = 0; + } + + return sLocalVer.clone(); + } + + public static int getVersionCode(Context c) { + int verCode = 0; + try { + verCode = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionCode; + } catch (NameNotFoundException e) { + + } + + return verCode; + } + + static void loadLoaclVer(Context c){ + try { + sLocalName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName; + } catch (NameNotFoundException e) { + throw new RuntimeException("Local Ver Package Error"); + } + + if( sLocalName == null ){ + throw new RuntimeException("Local Ver VersionName Not Exist"); + } + + Ver ver = VersionUtil.getVerFromStr(sLocalName); + sLocalVer = ver.toVerCode(); + } + + public static class Ver { + public int mMajor; + public int mMinor; + public int mBuild; + public boolean isSnapshot; + + public boolean bigThan(Ver v) { + return (mMajor > v.mMajor) || ( (mMajor == v.mMajor) && (mMinor > v.mMinor) ) + || ( (mMajor == v.mMajor) && (mMinor == v.mMinor) && (mBuild > v.mBuild) ); + } + + public boolean smallThan(Ver v) { + return (mMajor < v.mMajor) || ( (mMajor == v.mMajor) && (mMinor < v.mMinor) ) + || ( (mMajor == v.mMajor) && (mMinor == v.mMinor) && (mBuild < v.mBuild) ); + } + + + public boolean equals(Object o) { + if (o == null) { + return false; + } + Ver v = (Ver) o; + return (mMajor == v.mMajor) && (mMinor == v.mMinor) + && (mBuild == v.mBuild); + } + + @Override + public int hashCode() { + int result = mMajor; + result = 31 * result + mMinor; + result = 31 * result + mBuild; + return result; + } + + /** + * 升级时按此版本号请求升级号 by zhongyongsheng + * @return + */ + public String getVersionName(Context c) { +// if (isSnapshot) { +// return String.format("%d.%s.%d", 0, AppMetaDataUtil.getSvnBuildVersion(c), VersionUtil.getVersionCode(c)); +// } + return String.format("%d.%d.%d", mMajor, mMinor, mBuild); + } + + /** + * 关于中显示的版本号 by zhongyongsheng + * @return + */ + public String aboutDisplayName(Context c) { + if (isSnapshot) { + return String.format("%d.%d.%d-%s(%d.%s.%d)%s", mMajor, mMinor, mBuild, + ResUtil.getString(R.string.xchat_android_library_utils_versionutil_01), + 0, AppMetaDataUtil.getSvnBuildVersion(c), VersionUtil.getVersionCode(c), + BasicConfig.INSTANCE.isDebuggable() ? "D" : ""); + } + return String.format("%d.%d.%d", mMajor, mMinor, mBuild); + } + + /** + * 反馈上报的版本号 by zhongyongsheng + * @return + */ + public String feedbackVersionName(Context c) { + if (isSnapshot) { + return String.format("%d.%d.%d-dev(%d.%s.%d)%s", mMajor, mMinor, mBuild, + 0, AppMetaDataUtil.getSvnBuildVersion(c), VersionUtil.getVersionCode(c), + BasicConfig.INSTANCE.isDebuggable() ? "D" : ""); + } + return String.format("%d.%d.%d", mMajor, mMinor, mBuild); + } + + public String toString() { + if (isSnapshot) { + return String.format("%d.%d.%d(SNAPSHOT, Build %s)", mMajor, mMinor, mBuild, VersionUtil.getVersionCode(BasicConfig.INSTANCE.getAppContext())); + } + return String.format("%d.%d.%d", mMajor, mMinor, mBuild); + } + + public int[] toVerCode() { + int[] ver = new int[4]; + ver[0] = mMajor; + ver[1] = mMinor; + ver[2] = mBuild; + ver[3] = isSnapshot ? 1 : 0; + + return ver; + } + + public String getVersionNameWithoutSnapshot(){ + return String.format("%d.%d.%d", mMajor, mMinor, mBuild); + } + + /* + * version name for request of *.3g.yy.com + * @author zongbao + * @since 3.0 + * */ + public String getVersionNameFor3GReq() { + String versionName = getVersionNameWithoutSnapshot(); + if (isSnapshot || BasicConfig.INSTANCE.isDebuggable()) { + versionName += "_beta"; + } + return versionName; + } + + /** + * 获取manifiest的原始versionname + * @return + */ + public String getOriginalVersion() { + return sLocalName; + } + + }; + +} diff --git a/library/src/main/java/com/chwl/library/utils/anim/AnimUtils.java b/library/src/main/java/com/chwl/library/utils/anim/AnimUtils.java new file mode 100644 index 0000000..51a9bf8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/anim/AnimUtils.java @@ -0,0 +1,53 @@ +package com.chwl.library.utils.anim; + +import android.animation.Animator; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.LinearInterpolator; +import android.widget.TextView; + +/** + * Created by lijun on 2014/11/13. + */ +public class AnimUtils { + + private static final long BG_ANIM_DURATION = 15000; + + public static Animator createTextColorAnim(final TextView textView, int colorFrom, int colorTo) { + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animator) { + textView.setTextColor((Integer)animator.getAnimatedValue()); + } + }); + + return colorAnimation; + } + + @TargetApi(21) + private ObjectAnimator fade(final float alpha, View v) { + final ObjectAnimator fade = ObjectAnimator.ofFloat(v, "alpha", alpha); + fade.setInterpolator(AnimationUtils.loadInterpolator(v.getContext(), + android.R.anim.accelerate_decelerate_interpolator)); + fade.setDuration(400); + fade.start(); + return fade; + } + + public static ValueAnimator buildRotationAnimator() { + ValueAnimator bgAnimator = ValueAnimator.ofFloat(0f, 360f); + bgAnimator.setDuration(BG_ANIM_DURATION); // miliseconds + bgAnimator.setInterpolator(new LinearInterpolator()); + bgAnimator.setRepeatCount(ValueAnimator.INFINITE); + return bgAnimator; + } + + + +} diff --git a/library/src/main/java/com/chwl/library/utils/anim/PauseableAnimManager.java b/library/src/main/java/com/chwl/library/utils/anim/PauseableAnimManager.java new file mode 100644 index 0000000..4a0f720 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/anim/PauseableAnimManager.java @@ -0,0 +1,115 @@ +package com.chwl.library.utils.anim; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import com.chwl.library.utils.log.MLog; + +/** + * Created by lijun on 2015/7/24. + * + * 可暂停恢复的Animator管理,因为 ValueAnimator.pause, resume 接口需要 SDK19以上才能调用 + */ +public class PauseableAnimManager { + + private ValueAnimator valueAnimator; + private boolean isAnimPause; + private long mAnimPauseTime; + + private ValueAnimator.AnimatorUpdateListener updateListener; + private ValueAnimator.AnimatorListener animatorListener; + + public PauseableAnimManager(ValueAnimator valueAnimator) { + if (null == valueAnimator) { + throw new IllegalArgumentException("valueAnimator should not be null"); + } + + this.valueAnimator = valueAnimator; + init(); + } + + public void setUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { + this.updateListener = updateListener; + } + + public void setAnimatorListener(ValueAnimator.AnimatorListener animatorListener) { + this.animatorListener = animatorListener; + } + + public void start() { + valueAnimator.start(); + } + + public void pause() { + if (null != valueAnimator && valueAnimator.isRunning()) { + isAnimPause = true; + mAnimPauseTime = -1; + } + } + + public void resume() { + if (isAnimPause) { + MLog.debug(this, "resumed: %d", mAnimPauseTime); + valueAnimator.setCurrentPlayTime(mAnimPauseTime); + isAnimPause = false; + } + } + + public void cancel() { + valueAnimator.cancel(); + } + + public void end() { + valueAnimator.end(); + } + + private void init() { + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { +// MLog.debug(this, "onAnimationUpdate: %d", animation.getCurrentPlayTime()); + if (!isAnimPause) { + if (null != updateListener) { + updateListener.onAnimationUpdate(animation); + } + } else { + if (mAnimPauseTime < 0) { + mAnimPauseTime = animation.getCurrentPlayTime(); + MLog.debug(this, "mAnimPauseTime: %d", mAnimPauseTime); + } + } + } + }); + + valueAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + if (!isAnimPause && null != animatorListener) { + animatorListener.onAnimationStart(animation); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!isAnimPause && null != animatorListener) { + animatorListener.onAnimationEnd(animation); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (!isAnimPause && null != animatorListener) { + animatorListener.onAnimationCancel(animation); + } + } + + @Override + public void onAnimationRepeat(Animator animation) { + if (!isAnimPause && null != animatorListener) { + animatorListener.onAnimationRepeat(animation); + } + } + }); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/asynctask/AsyncTask.java b/library/src/main/java/com/chwl/library/utils/asynctask/AsyncTask.java new file mode 100644 index 0000000..07f3ddb --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/asynctask/AsyncTask.java @@ -0,0 +1,45 @@ +package com.chwl.library.utils.asynctask; + +import android.os.Build; +import android.os.HandlerThread; +import android.os.Looper; + +import com.chwl.library.utils.SafeDispatchHandler; + +/** + * 异步任务工具。 + */ +public final class AsyncTask { + private Looper mTaskLooper; + private SafeDispatchHandler mTaskHandler; + private HandlerThread mThread; + + public AsyncTask() { + this("AsyncTask"); + } + + public boolean quit() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return mThread.quitSafely(); + } else { + return mThread.quit(); + } + } + + public AsyncTask(String name) { + mThread = new HandlerThread(name); + mThread.start(); + + mTaskLooper = mThread.getLooper(); + mTaskHandler = new SafeDispatchHandler(mTaskLooper); + + } + + /** + * 执行任务,单位milliseconds + */ + public void execute(Runnable command) { + mTaskHandler.removeCallbacks(command); + mTaskHandler.post(command); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/asynctask/ScheduledTask.java b/library/src/main/java/com/chwl/library/utils/asynctask/ScheduledTask.java new file mode 100644 index 0000000..c4d94ac --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/asynctask/ScheduledTask.java @@ -0,0 +1,68 @@ +package com.chwl.library.utils.asynctask; + +import android.os.HandlerThread; +import android.os.Looper; + +import com.chwl.library.utils.SafeDispatchHandler; + +/** + * 执行延迟或定时任务。 + *

+ * Created by qinbo on 14/6/10. + */ +public final class ScheduledTask { + + private volatile Looper mTaskLooper; + private volatile SafeDispatchHandler mTaskHandler; + + /** + * 私有构造函数,防止误实例化 + */ + private ScheduledTask() { + HandlerThread thread = new HandlerThread("ScheduledTask"); + thread.start(); + + mTaskLooper = thread.getLooper(); + mTaskHandler = new SafeDispatchHandler(mTaskLooper); + + } + + /** + * 获取单实例 + */ + public static ScheduledTask getInstance() { + return SingletonHolder.INSTANCE; + } + + /** + * 延迟执行任务,单位milliseconds + */ + public boolean scheduledDelayed(Runnable command, long delay) { + mTaskHandler.removeCallbacks(command); + return mTaskHandler.postDelayed(command, delay); + + } + + /** + * 指定时刻执行任务,uptimeMillis(using the SystemClock.uptimeMillis() time-base) + */ + public boolean scheduledAtTime(Runnable command, long uptimeMillis) { + mTaskHandler.removeCallbacks(command); + return mTaskHandler.postAtTime(command, uptimeMillis); + } + + /** + * 停止计时器 + */ + public void removeCallbacks(Runnable command) { + mTaskHandler.removeCallbacks(command); + } + + /** + * 内部类, 用到时才会加载对象 + */ + private static class SingletonHolder { + public final static ScheduledTask INSTANCE = new ScheduledTask(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/Cache.java b/library/src/main/java/com/chwl/library/utils/cache/Cache.java new file mode 100644 index 0000000..433bb73 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/Cache.java @@ -0,0 +1,70 @@ +package com.chwl.library.utils.cache; + + +/** + * 缓存接口 + * + * @author 匡凌轩 V1.0 + */ +public interface Cache { + + /** + * 异步get,无阻塞 + * + * @param key 查询键 + * @param returnCallback 返回值回调处理 + */ + public void get(String key, ReturnCallback returnCallback); + + /** + * 异步get,无阻塞 + * + * @param key 查询键 + * @param returncallback 返回值回调处理 + * @param errorCallback 错误回调处理 + */ + public void get(String key, ReturnCallback returncallback, + ErrorCallback errorCallback); + +// /** +// * 同步get,会阻塞,请在非主线程中使用 +// * @param key 查询键 +// * @return 返回值 +// */ +// public String get(String key); + + /** + * 异步put,暂时不需要管结果 + * + * @param key 存储键 + * @param value 存储值 + */ + public void put(String key, Object value); + + /** + * 异步put,暂时不需要管结果 + * + * @param key 存储键 + * @param value 存储值 + * @param expire 超时时间 + */ + public void put(String key, Object value, long expire); + + /** + * 删除 + * + * @param key + */ + public void remove(String key); + + /** + * 全部清空 + */ + public void clear(); + + + //sync method + + Object getSync(String key) throws CacheException; + +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/CacheClient.java b/library/src/main/java/com/chwl/library/utils/cache/CacheClient.java new file mode 100644 index 0000000..69a59b1 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/CacheClient.java @@ -0,0 +1,326 @@ +package com.chwl.library.utils.cache; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.TimeUtils; +import com.chwl.library.utils.asynctask.AsyncTask; +import com.chwl.library.utils.json.JsonParser; +import com.chwl.library.utils.log.MLog; + +import java.io.Serializable; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 缓存客户端 + */ +@SuppressLint("HandlerLeak") +public class CacheClient implements Cache { + + private long defaultExpire; + + private AsyncTask asyncTask = new AsyncTask("CacheClient"); + + private Map> manager = new ConcurrentHashMap>(); + + private CacheManager cacheManager; + + /** + * 保证资源唯一 + */ + private String uri; + + private Handler handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + CallbackWrapper wrapper = (CallbackWrapper) msg.obj; + ReturnCallback returnCallback = wrapper.getReturnCallback(); + if (returnCallback != null) { + try { + wrapper.getReturnCallback().onReturn(wrapper.getData()); + } catch (Exception e) { + MLog.error(this, e); + } + } + ErrorCallback errorCallback = wrapper.getErrorCallback(); + if (errorCallback != null) { + try { + wrapper.getErrorCallback().onError(wrapper.getError()); + } catch (Exception e) { + MLog.error(this, e); + } + } + } + }; + + protected CacheClient(String uri) { + this(uri, TimeUtils.MINUTES_OF_HOUR * TimeUtils.SECONDS_OF_MINUTE * TimeUtils.MILLIS_OF_SECOND); + } + + protected CacheClient(String uri, long defaultExpire) { + this.defaultExpire = defaultExpire; + this.uri = uri; + this.cacheManager = new CacheManager(uri); + } + + @Override + public Object getSync(String key) throws CacheException { + if (StringUtils.isBlank(key)) { + return null; + } + + final String mKey = key; + Object data = ""; + CacheException error = null; + try { + //TODO read json + String json = cacheManager.getCache(mKey); + + CachePacket packet = JsonParser.parseJsonObject(json, CachePacket.class); + data = packet.getContent(); + } catch (NoSuchKeyException e) { + error = e; + MLog.error(this, e); + } catch (Exception e) { + error = new CacheException(mKey, "Wrap otherwise exceptions", e); + MLog.error(this, error); + } + + if (null != error) { + throw error; + } + + return data; + } + + @Override + public void get(String key, ReturnCallback returncallback) { + get(key, returncallback, null); + } + + @Override + public void get(String key, ReturnCallback returncallback, ErrorCallback errorCallback) { + if (StringUtils.isBlank(key)) { + return; + } + final String mKey = key; + BlockingQueue handlers = manager.get(mKey); + if (handlers == null) { + handlers = new LinkedBlockingQueue(); + } + CallbackWrapper wrapper = new CallbackWrapper(); + wrapper.setReturnCallback(returncallback); + wrapper.setErrorCallback(errorCallback); + handlers.add(wrapper); + manager.put(mKey, handlers); + + asyncTask.execute(new Runnable() { + @Override + public void run() { + Object data = ""; + CacheException error = null; + BlockingQueue handlers = manager.get(mKey); + if (handlers.isEmpty()) { + return; + } + try { + //TODO read json + String json = cacheManager.getCache(mKey); + + CachePacket packet = JsonParser.parseJsonObject(json, CachePacket.class); + data = packet.getContent(); + } catch (NoSuchKeyException e) { + error = e; + MLog.error(this, e); + } catch (Exception e) { + error = new CacheException(mKey, "Wrap otherwise exceptions", e); + MLog.error(this, error); + } + + for (; ; ) { + CallbackWrapper wrapper = handlers.poll(); + if (wrapper == null) { + break; + } + wrapper.setData(data); + wrapper.setError(error); + Message msg = Message.obtain(); + msg.obj = wrapper; + handler.sendMessage(msg); + } + } + }); + } + + @Override + public void put(String key, Object value) { + put(key, value, defaultExpire); + } + + @Override + public void put(String key, Object value, long expire) { + if (StringUtils.isBlank(key)) { + return; + } + final String mKey = key; + final long mexpire = expire; + CacheHeader header = new CacheHeader(key, expire, System.currentTimeMillis()); + CachePacket packet = new CachePacket(header, value); + final String json = JsonParser.toJson(packet); + asyncTask.execute(new Runnable() { + @Override + public void run() { + //TODO Save json + cacheManager.putCache(mKey, json, mexpire); + } + }); + } + + public void remove(String key) { + // TODO Auto-generated method stub + cacheManager.remove(key); + } + + public void clear() { + // TODO Auto-generated method stub + cacheManager.clear(); + } + + public String getUri() { + return uri; + } + + public class CallbackWrapper { + + private Object data; + + private CacheException error; + + private ReturnCallback returnCallback; + + private ErrorCallback errorCallback; + + public CallbackWrapper() { + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public ReturnCallback getReturnCallback() { + return returnCallback; + } + + public void setReturnCallback(ReturnCallback returnCallback) { + this.returnCallback = returnCallback; + } + + public ErrorCallback getErrorCallback() { + return errorCallback; + } + + public void setErrorCallback(ErrorCallback errorCallback) { + this.errorCallback = errorCallback; + } + + public CacheException getError() { + return error; + } + + public void setError(CacheException error) { + this.error = error; + } + + + } + + /** + * 缓存协议头 + * + */ + public class CacheHeader implements Serializable { + + private String key; + + private long expired; + + private long createTime; + + public CacheHeader(String key, long expired, long createTime) { + super(); + this.key = key; + this.expired = expired; + this.createTime = createTime; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public long getExpired() { + return expired; + } + + public void setExpired(long expired) { + this.expired = expired; + } + + public long getCreateTime() { + return createTime; + } + + public void setCreateTime(long createTime) { + this.createTime = createTime; + } + + + } + + /** + * 缓存协议包 + * + */ + public class CachePacket implements Serializable { + + private CacheHeader header; + + private Object content; + + public CachePacket(CacheHeader header, Object content) { + this.header = header; + this.content = content; + } + + public CacheHeader getHeader() { + return header; + } + + public void setHeader(CacheHeader header) { + this.header = header; + } + + public Object getContent() { + return content; + } + + public void setContents(Object content) { + this.content = content; + } + + + } +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/CacheClientFactory.java b/library/src/main/java/com/chwl/library/utils/cache/CacheClientFactory.java new file mode 100644 index 0000000..79db6b8 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/CacheClientFactory.java @@ -0,0 +1,69 @@ +package com.chwl.library.utils.cache; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 缓存客户端工厂 + * + * @author 匡凌轩 V1.0 + */ +public class CacheClientFactory { + + private static final String PUBLIC_URI = "dataCache" + File.separator + "public" + File.separator; + private static final String PRIVATE_URI = "dataCache" + File.separator + "private" + File.separator; + private static Map map = new ConcurrentHashMap(); + + /** + * 获取公共数据CacheClient,根据业务需求只会同时存在1个Public CacheClient + * + * @return CacheClient + */ + public static CacheClient getPublic() { + CacheClient client = map.get(CacheType.PUBLIC); + if (client == null) { + String uri = PUBLIC_URI; + client = new CacheClient(uri); + map.put(CacheType.PUBLIC, client); + } + return client; + } + + /** + * 获取私有数据CacheClient,根据业务需求只会同时存在1个Private CacheClient + * 如果为null,请使用前调用register注册一次 + * + * @return CacheClient + */ + public static CacheClient getPrivate() { + CacheClient client = map.get(CacheType.PRIVATE); + return client; + } + + /** + * 删除私有CacheClient。例如:注销切换用户时请清除当前用户Private CacheClient + */ + public static void removePrivate() { + map.remove(CacheType.PRIVATE); + } + + /** + * 注册私有CacheClient + * + * @param uri + */ + public static void registerPrivate(String uri) { + uri = PRIVATE_URI + uri; + CacheClient client = new CacheClient(uri); + map.put(CacheType.PRIVATE, client); + } + + /** + * 缓冲类型 + */ + public enum CacheType { + PRIVATE,//登陆者私有数据 + PUBLIC//公共数据 + } +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/CacheException.java b/library/src/main/java/com/chwl/library/utils/cache/CacheException.java new file mode 100644 index 0000000..3fc35c3 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/CacheException.java @@ -0,0 +1,30 @@ +package com.chwl.library.utils.cache; + +public class CacheException extends Exception { + + private static final long serialVersionUID = 2606810248388215947L; + + private String key; + + public CacheException() { + + } + + public CacheException(String key, String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + this.key = key; + } + + public CacheException(String key, String detailMessage) { + super(detailMessage); + this.key = key; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/CacheManager.java b/library/src/main/java/com/chwl/library/utils/cache/CacheManager.java new file mode 100644 index 0000000..7735daf --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/CacheManager.java @@ -0,0 +1,168 @@ +package com.chwl.library.utils.cache; + +import android.content.Context; + +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.codec.MD5Utils; +import com.chwl.library.utils.config.BasicConfig; +import com.chwl.library.utils.file.StorageUtils; +import com.chwl.library.utils.json.JsonParser; +import com.chwl.library.utils.log.MLog; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class CacheManager { + private static final String TAG = "CacheManager"; + //LruCache + private static final int MEM_MAX_SIZE = 1 * 1024 * 1024;// MEM 1MB + //DiskLruCache + private static final int DISK_MAX_SIZE = 5 * 1024 * 1024;// SD 5MB + private static final String CACHE_PATH = "cacheDir"; + private StringLruCache mMemoryCache = null; + private StringDiskCache mDiskCacke = null; + + private Context mContext = null; + + public CacheManager(String fileName) { + mContext = BasicConfig.INSTANCE.getAppContext(); + + //内存缓存 + mMemoryCache = new StringLruCache(MEM_MAX_SIZE) { + @Override + protected int sizeOf(String key, String value) { + int count = value.getBytes().length; + return count; + } + + @Override + protected void entryRemoved(boolean evicted, String key, + String oldValue, String newValue) { + super.entryRemoved(evicted, key, oldValue, newValue); + } + }; + + + //sdcard或者内置存储 + File cacheDir = StorageUtils.getCacheDirectory(mContext, CACHE_PATH + File.separator + fileName); + mDiskCacke = StringDiskCache.openCache(cacheDir, DISK_MAX_SIZE); + } + +// public static CacheManager instance() +// { +// if(mInstance==null) { +// mInstance = new CacheManager(); +// } +// return mInstance; +// } + + + public boolean putCache(String key, String json, long expire) { + String md5Key = MD5Utils.getMD5String(key); + if (mDiskCacke != null) { + MLog.info(TAG, "put json to SD key = " + key); + mDiskCacke.putText(md5Key, json); + } + if (mMemoryCache != null) { + MLog.info(TAG, "put json to Memory key = " + key); + mMemoryCache.put(md5Key, json); + } + return false; + } + + public String getCache(String key) throws NoSuchKeyException, IOException { + //线程的优先级,设置低点对主线程 + //Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + if (StringUtils.isBlank(key)) { + return null; + } + String md5Key = MD5Utils.getMD5String(key); + + String json = ""; + if (mMemoryCache != null) { + json = mMemoryCache.get(md5Key); + if (json != null) { + CacheClient.CachePacket packet = JsonParser.parseJsonObject(json, CacheClient.CachePacket.class); + long expiredTime = System.currentTimeMillis() - packet.getHeader().getCreateTime(); + + if (expiredTime > packet.getHeader().getExpired()) { + remove(key); + return null; + } + MLog.info(TAG, "get Json from mem: key = " + key); + return json; + } + } + //内存中没有 + if (mDiskCacke != null) { + try { + json = mDiskCacke.get(md5Key); + if (json != null) { + mMemoryCache.put(md5Key, json); + MLog.info(TAG, "get Json from sd: key = " + key); + return json; + } + } catch (final FileNotFoundException e) { + throw new NoSuchKeyException(key, "no such key"); + } catch (final IOException e) { + throw e; + } + } + return null; + } + + /** + * 清空key值,释放sd和内存的数据 + * + * @param key + */ + public void remove(String key, String path) { + if (StringUtils.isBlank(key)) { + return; + } + String md5Key = MD5Utils.getMD5String(key); + + if (mMemoryCache != null) { + mMemoryCache.remove(md5Key); + } + if (mDiskCacke != null) { + //内存中没有 + mDiskCacke.clearCache(md5Key); + } + } + + /** + * 清空key值,释放sd和内存的数据 + * + * @param key + */ + public void remove(String key) { + if (StringUtils.isBlank(key)) { + return; + } + String md5Key = MD5Utils.getMD5String(key); + + if (mMemoryCache != null) { + mMemoryCache.remove(md5Key); + } + if (mDiskCacke != null) { + //内存中没有 + mDiskCacke.clearCache(md5Key); + } + } + + /** + * 清空anything + */ + public void clear() { + if (mMemoryCache != null) { + mMemoryCache.evictAll(); + } + if (mDiskCacke != null) { + //内存中没有 + mDiskCacke.clearCache(); + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/ErrorCallback.java b/library/src/main/java/com/chwl/library/utils/cache/ErrorCallback.java new file mode 100644 index 0000000..7771c83 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/ErrorCallback.java @@ -0,0 +1,12 @@ +package com.chwl.library.utils.cache; + +public interface ErrorCallback { + + /** + * 错误回调处理 + * + * @param e IOExcepiton, + * @throws Exception + */ + public void onError(CacheException e) throws Exception; +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/NoSuchKeyException.java b/library/src/main/java/com/chwl/library/utils/cache/NoSuchKeyException.java new file mode 100644 index 0000000..15400bb --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/NoSuchKeyException.java @@ -0,0 +1,19 @@ +package com.chwl.library.utils.cache; + +public class NoSuchKeyException extends CacheException { + + private static final long serialVersionUID = 6921968875814792791L; + + + public NoSuchKeyException() { + + } + + public NoSuchKeyException(String key, String detailMessage, Throwable throwable) { + super(key, detailMessage, throwable); + } + + public NoSuchKeyException(String key, String detailMessage) { + super(key, detailMessage); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/ReturnCallback.java b/library/src/main/java/com/chwl/library/utils/cache/ReturnCallback.java new file mode 100644 index 0000000..9a2f4aa --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/ReturnCallback.java @@ -0,0 +1,6 @@ +package com.chwl.library.utils.cache; + +public interface ReturnCallback { + + public void onReturn(Object data) throws Exception; +} diff --git a/library/src/main/java/com/chwl/library/utils/cache/StringDiskCache.java b/library/src/main/java/com/chwl/library/utils/cache/StringDiskCache.java new file mode 100644 index 0000000..827ba74 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/StringDiskCache.java @@ -0,0 +1,350 @@ +package com.chwl.library.utils.cache; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.common.file.FileHelper; +import com.chwl.library.utils.ResUtil; +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.json.JsonParser; +import com.chwl.library.utils.log.MLog; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * 用于保存数据到sdcard卡,仅处理String数据 + * + * @author wshao + */ +public class StringDiskCache { + private static final String TAG = "DiskLruCache"; + private static final boolean DEBUG = false; + + private static final String CACHE_FILENAME_PREFIX = ""; + /** + * A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX + * prepended. + */ + private static final FilenameFilter CACHE_FILE_FILTER = new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(CACHE_FILENAME_PREFIX); + } + }; + private static final int MAX_REMOVALS = 4; + private static final int INITIAL_CAPACITY = 32; + private static final float LOAD_FACTOR = 0.75f; + private final Map mLinkedHashMap = + Collections.synchronizedMap(new LinkedHashMap( + INITIAL_CAPACITY, LOAD_FACTOR, true)); + private final File mCacheDir; + private final int maxCacheItemSize = 8192; // 8192 item default + + private int cacheSize = 0; + private int cacheByteSize = 0; + private long maxCacheByteSize = 1024 * 1024 * 16; // 16MB default + + private StringDiskCache(File cacheDir, long maxByteSize) { + mCacheDir = cacheDir; + maxCacheByteSize = maxByteSize; + } + + /** + * Used to fetch an instance of DiskLruCache. + */ + public static StringDiskCache openCache(File cacheDir, long maxByteSize) { + if (cacheDir == null) { + cacheDir = new File(FileHelper.getRootCacheDir().getAbsolutePath() + File.separator + "cacheDir"); + } + + if (!cacheDir.exists()) { + if (!cacheDir.mkdirs()) { + MLog.error(TAG, "ERROR: Cannot create dir " + cacheDir.toString() + "!!!"); + return null; + } + } + + if (cacheDir.isDirectory() && cacheDir.canWrite() + && getUsableSpace(cacheDir) > maxByteSize) { + MLog.info(TAG, "cacheDir :" + cacheDir.toString()); + return new StringDiskCache(cacheDir, maxByteSize); + } + + return null; + } + + private static void clearCache(File cacheDir) { + final File[] files = cacheDir.listFiles(CACHE_FILE_FILTER); + for (int i = 0; i < files.length; i++) { + files[i].delete(); + } + } + + /** + * Creates a constant cache file path given a target cache directory and an image key. + * + * @param cacheDir + * @param key + * @return + */ + public static String createFilePath(File cacheDir, String key) { + try { + // Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for + // this example + return cacheDir.getPath() + File.separator + + CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8"); + } catch (final UnsupportedEncodingException e) { + MLog.error(TAG, "createFilePath - " + e); + } + + return null; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private static long getUsableSpace(File path) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return path.getUsableSpace(); + } + final StatFs stats = new StatFs(path.getPath()); + return stats.getBlockSizeLong() * stats.getAvailableBlocksLong(); + } + + public void putText(String key, String data) { + try { + final String file = createFilePath(mCacheDir, key); + if (StringUtils.isNotBlank(file)) { + synchronized (file) { + if (writeTextFile(data, file)) { + put(key, file); + if (DEBUG) { + Log.d(TAG, "put - Added cache file, " + file); + } + + flushCache(); + } + } + } + } catch (final FileNotFoundException e) { + MLog.error(TAG, "Error in put: " + e.getMessage()); + } catch (final IOException e) { + MLog.error(TAG, "Error in put: " + e.getMessage()); + } + } + + private void put(String key, String file) { + mLinkedHashMap.put(key, file); + cacheSize = mLinkedHashMap.size(); + cacheByteSize += new File(file).length(); + } + + /** + * Flush the cache, removing oldest entries if the total size is over the specified cache size. + * Note that this isn't keeping track of stale files in the cache directory that aren't in the + * HashMap. If the images and keys in the disk cache change often then they probably won't ever + * be removed. + */ + private void flushCache() { + Entry eldestEntry; + File eldestFile; + long eldestFileSize; + int count = 0; + + while (count < MAX_REMOVALS && + (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) { + eldestEntry = mLinkedHashMap.entrySet().iterator().next(); + eldestFile = new File(eldestEntry.getValue()); + eldestFileSize = eldestFile.length(); + mLinkedHashMap.remove(eldestEntry.getKey()); + eldestFile.delete(); + cacheSize = mLinkedHashMap.size(); + cacheByteSize -= eldestFileSize; + count++; + if (DEBUG) { + Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + + eldestFileSize); + } + } + } + + /** + * Get an image from the disk cache. + * + * @param key The unique identifier for the json + * @return The json or null if not found + */ + public String get(String key) throws FileNotFoundException, IOException { + String file = mLinkedHashMap.get(key); + if (StringUtils.isBlank(file)) { + file = createFilePath(mCacheDir, key); + } + + if (StringUtils.isNotBlank(file)) { + synchronized (file) { + File fis = new File(file); + if (fis.exists()) { + if (DEBUG) { + Log.d(TAG, "Disk cache hit (existing file)"); + } + try { + + String json = checkExpire(fis); + if (StringUtils.isNotBlank(json)) { + put(key, file); + } + return json; + } catch (final FileNotFoundException e) { + MLog.error(TAG, "Error in get: " + e.getMessage()); + throw new FileNotFoundException(); + } catch (final IOException e) { + MLog.error(TAG, "Error in get: " + e.getMessage()); + throw e; + } + } + } + } + throw new FileNotFoundException(); + } + + private String checkExpire(File newfile) throws FileNotFoundException, UnsupportedEncodingException { + FileInputStream fis = new FileInputStream(newfile); + String json = readIs2Bytes(fis); + + if (json != null) { + CacheClient.CachePacket packet = JsonParser.parseJsonObject(json, CacheClient.CachePacket.class); + long expiredTime = System.currentTimeMillis() - packet.getHeader().getCreateTime(); + + if (expiredTime > packet.getHeader().getExpired()) { + newfile.delete(); + return null; + } + } + return json; + } + + /** + * 将输入流数据读取到输出流当中 + */ + private OutputStream readIs2Os(InputStream is, OutputStream os) { + try { + byte[] bytes = new byte[1024]; + int length = 0; + while ((length = is.read(bytes)) != -1) { + os.write(bytes, 0, length); + } + try { + is.close(); + os.close(); + } catch (Exception e) { + } + } catch (IOException e) { + e.printStackTrace(); + } + return os; + } + + /** + * 将输入流数据读取到输出流当中 + */ + public String readIs2Bytes(InputStream is) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + readIs2Os(is, baos); + return new String(baos.toByteArray(), "utf-8"); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * Checks if a specific key exist in the cache. + * + * @param key The unique identifier for the json + * @return true if found, false otherwise + */ + public boolean containsKey(String key) { + // See if the key is in our HashMap + if (mLinkedHashMap.containsKey(key)) { + return true; + } + return false; + } + + /** + * Removes all disk cache entries from this instance cache dir + */ + public void clearCache() { + clearCache(mCacheDir); + } + + public void clearCache(String key) { + String file = mLinkedHashMap.get(key); + if (StringUtils.isBlank(file)) { + file = createFilePath(mCacheDir, key); + } + + if (StringUtils.isNotBlank(file)) { + synchronized (file) { + File newfile = new File(file); + if (newfile.exists()) { + newfile.delete(); + } + } + } + } + + /** + * Create a constant cache file path using the current cache directory and an image key. + * + * @param key + * @return + */ + public String createFilePath(String key) { + return createFilePath(mCacheDir, key); + } + + private boolean writeTextFile(String Str, String file) throws IOException, FileNotFoundException { + if (StringUtils.isBlank(Str)) { + return false; + } + OutputStream out = null; + long begin0 = System.currentTimeMillis(); + + out = new BufferedOutputStream(new FileOutputStream(file)); + out.write(Str.getBytes("UTF-8")); + out.flush(); + + if (out != null) { + try { + out.close(); + } catch (Exception e) { + } + + long end0 = System.currentTimeMillis(); + MLog.info(TAG, file + ResUtil.getString(R.string.utils_cache_stringdiskcache_01) + (end0 - begin0) + ResUtil.getString(R.string.utils_cache_stringdiskcache_02)); + } + + return true; + } + +} + diff --git a/library/src/main/java/com/chwl/library/utils/cache/StringLruCache.java b/library/src/main/java/com/chwl/library/utils/cache/StringLruCache.java new file mode 100644 index 0000000..3f21a34 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/cache/StringLruCache.java @@ -0,0 +1,311 @@ +package com.chwl.library.utils.cache; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Static library version of {@code android.util.LruCache}. Used to write apps + * that run on API levels prior to 12. When running on API level 12 or above, + * this implementation is still used; it does not try to switch to the + * framework's implementation. See the framework SDK documentation for a class + * overview. + */ +public class StringLruCache { + private final LinkedHashMap map; + + /** + * Size of this cache in units. Not necessarily the number of elements. + */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public StringLruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + private void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + *

+ *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { + } + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + *

+ *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + *

+ *

If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + *

+ *

An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override + public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/Base64Utils.java b/library/src/main/java/com/chwl/library/utils/codec/Base64Utils.java new file mode 100644 index 0000000..2f92231 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/Base64Utils.java @@ -0,0 +1,739 @@ +package com.chwl.library.utils.codec; + +/** + * Created by Zhanghuiping on 14/6/9. + */ + + +import com.chwl.library.BuildConfig; + +import java.io.UnsupportedEncodingException; + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs 2045 and 3548. + */ +public class Base64Utils { + /** + * 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 android.util.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 + // -------------------------------------------------------- + + private Base64Utils() { + } // don't instantiate + + // -------------------------------------------------------- + // 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; + } + + /** + * 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); + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * 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 outputLen = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.doPadding) { + if (len % 3 > 0) { + outputLen += 4; + } + } else { + switch (len % 3) { + case 0: + break; + case 1: + outputLen += 2; + break; + case 2: + outputLen += 3; + break; + } + } + + // Account for the newlines, if any. + if (encoder.doNewline && len > 0) { + outputLen += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.doCr ? 2 : 1); + } + + encoder.output = new byte[outputLen]; + encoder.process(input, offset, len, true); + + if (BuildConfig.DEBUG && !(encoder.op == outputLen)) { + throw new RuntimeException(); + } + + return encoder.output; + } + + /* 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); + } + + /* 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; + final private int[] alphabet; + /** + * 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; + + 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; + } + } + + /* 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', '-', '_', + }; + + public final boolean doPadding; + public final boolean doNewline; + public final boolean doCr; + private final byte[] tail; + private final byte[] alphabet; + /* package */ int tailLen; + private int count; + + public Encoder(int flags, byte[] output) { + this.output = output; + + doPadding = (flags & NO_PADDING) == 0; + doNewline = (flags & NO_WRAP) == 0; + doCr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = doNewline ? 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 (doCr) 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 (doCr) 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 (doPadding) { + output[op++] = '='; + output[op++] = '='; + } + if (doNewline) { + if (doCr) 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 (doPadding) { + output[op++] = '='; + } + if (doNewline) { + if (doCr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (doNewline && op > 0 && count != LINE_GROUPS) { + if (doCr) output[op++] = '\r'; + output[op++] = '\n'; + } + + if (BuildConfig.DEBUG && !(tailLen == 0)) { + throw new RuntimeException(); + } + if (BuildConfig.DEBUG && !(p == len)) { + throw new RuntimeException(); + } + } 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; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/CipherHelper.java b/library/src/main/java/com/chwl/library/utils/codec/CipherHelper.java new file mode 100644 index 0000000..4ce0612 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/CipherHelper.java @@ -0,0 +1,186 @@ +package com.chwl.library.utils.codec; + +import android.util.Base64; + +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.log.MLog; + +import java.nio.ByteBuffer; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * Created by ping on 14/9/10. + */ +public class CipherHelper { + private static CipherHelper mInstance; + private DESedeCipher mCipher; + private ByteBuffer mScratchBuffer = ByteBuffer.allocate(8); + + public CipherHelper() { + mCipher = new DESedeCipher(); + } + + public synchronized static CipherHelper getInstance() { + if (mInstance == null) { + mInstance = new CipherHelper(); + } + return mInstance; + } + + public synchronized String encryptString(String plain) { + if (StringUtils.isBlank(plain)) { + return plain; + } + + byte[] content = plain.getBytes(); + byte[] cipherByte = mCipher.encrypt(content); + return Base64.encodeToString(cipherByte, Base64.NO_WRAP); + } + + public synchronized String encryptInt(int plain) { + mScratchBuffer.clear(); + mScratchBuffer.putInt(plain); + byte[] cipherByte = mCipher.encrypt(mScratchBuffer.array(), 0, 4); + return new String(Base64.encode(cipherByte, Base64.NO_WRAP)); + } + + public synchronized String encryptLong(long plain) { + mScratchBuffer.clear(); + mScratchBuffer.putLong(plain); + byte[] cipherByte = mCipher.encrypt(mScratchBuffer.array(), 0, 8); + return new String(Base64.encode(cipherByte, Base64.NO_WRAP)); + } + + public synchronized String decryptString(String cipher) { + if (StringUtils.isBlank(cipher)) { + return cipher; + } + + byte[] content = cipher.getBytes(); + byte[] cipherByte = Base64.decode(content, Base64.NO_WRAP); + return new String(mCipher.decrypt(cipherByte)); + } + + public synchronized int decryptInt(String cipher, int defaultVal) { + if (StringUtils.isBlank(cipher)) { + return defaultVal; + } + + byte[] cipherByte = Base64.decode(cipher.getBytes(), Base64.NO_WRAP); + byte[] plainByte = mCipher.decrypt(cipherByte); + + if (plainByte.length > 4) { + MLog.error(this, "decrypt int error, byte length:%d", plainByte.length); + return defaultVal; + } + + mScratchBuffer.clear(); + mScratchBuffer.put(plainByte); + mScratchBuffer.flip(); + return mScratchBuffer.getInt(); + } + + public synchronized long decryptLong(String cipher, long defaultVal) { + if (StringUtils.isBlank(cipher)) { + return defaultVal; + } + + byte[] cipherByte = Base64.decode(cipher.getBytes(), Base64.NO_WRAP); + byte[] plainByte = mCipher.decrypt(cipherByte); + + if (plainByte.length > 8) { + MLog.error(this, "decrypt long error, byte length:%d", plainByte.length); + return defaultVal; + } + + mScratchBuffer.clear(); + mScratchBuffer.put(plainByte); + mScratchBuffer.flip(); + return mScratchBuffer.getLong(); + } + + //also called 3DES + public static class DESedeCipher { + private static final String ALGORITHM = "DESede/ECB/PKCS5Padding"; + private static final byte[] CIPHER_KEY = {0x61, 0x65, 0x66, 0x64, 0x40, 0x39, 0x33, 0x66, + 0x31, 0x2D, 0x35, 0x24, 0x61, 0x38, 0x34, 0x21, 0x65, 0x61, 0x32, 0x23, 0x39, 0x33, 0x31, 0x66}; + private Cipher mEncrypt; + private Cipher mDecrypt; + + public DESedeCipher() { + initCipher(CIPHER_KEY); + } + + private void initCipher(byte[] key) { + try { + SecretKeySpec skeySpec = new SecretKeySpec(key, ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec); + mEncrypt = cipher; + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + + try { + SecretKeySpec skeySpec = new SecretKeySpec(key, ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, skeySpec); + mDecrypt = cipher; + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + } + + public byte[] encrypt(byte[] data) { + if (mEncrypt != null) { + try { + return mEncrypt.doFinal(data); + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + } + + return data; + } + + public byte[] decrypt(byte[] data) { + if (mDecrypt != null) { + try { + return mDecrypt.doFinal(data); + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + } + + return data; + } + + //the length of input and output may not equal + public byte[] encrypt(byte[] data, int start, int count) { + if (mEncrypt != null) { + try { + return mEncrypt.doFinal(data, start, count); + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + } + + return null; + } + + public byte[] decrypt(byte[] data, int start, int count) { + if (mDecrypt != null) { + try { + return mDecrypt.doFinal(data, start, count); + } catch (Exception e) { + MLog.error("AESCipher", e.toString()); + } + } + + return null; + } + + } +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/DES3Utils.java b/library/src/main/java/com/chwl/library/utils/codec/DES3Utils.java new file mode 100644 index 0000000..b220a0e --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/DES3Utils.java @@ -0,0 +1,66 @@ +package com.chwl.library.utils.codec; + +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESedeKeySpec; +import javax.crypto.spec.IvParameterSpec; + +/** + * Created by Zhanghuiping on 14/6/5. + */ + +public class DES3Utils { + private final static String SECRET_KEY = "asfje87sj08$%^ewj937#@#4jsn"; + private final static String IV = "01234567"; + private final static String ENCODING = "utf-8"; + + public static String encrypt(String plainText) throws Exception { + return encrypt(plainText, SECRET_KEY); + } + + /** + * @param plainText plainText + * @param secretKey secretKey , min length is 24(DESedeKeySpec.DES_EDE_KEY_LEN) + * @return encryptText + * @throws Exception + */ + public static String encrypt(String plainText, String secretKey) throws Exception { + if (plainText == null || plainText.length() == 0 || secretKey == null || secretKey.length() == 0) { + return null; + } + Key deskey = null; + DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("desede"); + deskey = keyFactory.generateSecret(spec); + + Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding"); + IvParameterSpec ips = new IvParameterSpec(IV.getBytes()); + cipher.init(Cipher.ENCRYPT_MODE, deskey, ips); + byte[] encryptData = cipher.doFinal(plainText.getBytes(ENCODING)); + return Base64Utils.encodeToString(encryptData, Base64Utils.NO_WRAP); + } + + public static String decrypt(String encryptText) throws Exception { + return decrypt(encryptText, SECRET_KEY); + } + + public static String decrypt(String encryptText, String secretKey) throws Exception { + if (encryptText == null || encryptText.length() == 0 || secretKey == null || secretKey.length() == 0) { + return null; + } + Key deskey = null; + DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("desede"); + deskey = keyFactory.generateSecret(spec); + Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding"); + IvParameterSpec ips = new IvParameterSpec(IV.getBytes()); + cipher.init(Cipher.DECRYPT_MODE, deskey, ips); + + byte[] decryptData = cipher.doFinal(Base64Utils.decode(encryptText, Base64Utils.NO_WRAP)); + + return new String(decryptData, ENCODING); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/DESUtils.java b/library/src/main/java/com/chwl/library/utils/codec/DESUtils.java new file mode 100644 index 0000000..3e05805 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/DESUtils.java @@ -0,0 +1,37 @@ +package com.chwl.library.utils.codec; + + +import com.chwl.library.utils.StringUtils; +import com.secure.encipher.EncipherLib; + + +public class DESUtils { + + //客户端加密 + public static String DESAndBase64Encrypt(String dataToEncrypt) throws Exception { + return EncipherLib.INSTANCE.encryptTextDef1(dataToEncrypt); + } + + public static String DESAndBase64(String psw) { + String pwd = ""; + try { + pwd = DESUtils.DESAndBase64Encrypt(psw); + } catch (Exception e) { + e.printStackTrace(); + } + return pwd; + } + + //服务端解密 + public static String DESAndBase64Decrypt(String dataBase64) throws Exception { + if (StringUtils.isEmpty(dataBase64)) return null; + return EncipherLib.INSTANCE.decryptTextDef1(dataBase64); + } + + + //解密 + public static String decryptDef2(String dataBase64) throws Exception { + if (StringUtils.isEmpty(dataBase64)) return null; + return EncipherLib.INSTANCE.decryptTextDef2(dataBase64); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/codec/MD5Utils.java b/library/src/main/java/com/chwl/library/utils/codec/MD5Utils.java new file mode 100644 index 0000000..36c8b28 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/MD5Utils.java @@ -0,0 +1,105 @@ +package com.chwl.library.utils.codec; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by Zhanghuiping on 14/6/5. + */ +public class MD5Utils { + private static final String TAG = "MD5Utils"; + + public static String getFileMd5String(String path) throws IOException { + if (path == null || path.length() == 0) { + return null; + } + File big = new File(path); + return getFileMD5String(big); + } + + public static String getFileMD5String(File file) throws IOException { + if (file == null || !file.exists()) { + return null; + } + InputStream fis = new FileInputStream(file); + byte[] buffer = new byte[2048]; + int numRead = 0; + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + while ((numRead = fis.read(buffer)) > 0) { + md5.update(buffer, 0, numRead); + } + fis.close(); + return md5ToString(md5.digest()); + } catch (Exception e) { + return null; + } + } + + public static String md5ToString(byte[] md5Bytes) { + StringBuilder hexValue = new StringBuilder(); + for (byte md5Byte : md5Bytes) { + int val = ((int) md5Byte) & 0xff; + if (val < 16) { + hexValue.append("0"); + } + hexValue.append(Integer.toHexString(val)); + } + return hexValue.toString(); + } + + public static String getMD5String(String s) { + if (s == null) { + return null; + } + return getMD5String(s.getBytes()); + } + + public static String getMD5String(byte[] bytes) { + if (bytes == null) { + return null; + } + String md5 = null; + try { + + MessageDigest messagedigest = MessageDigest.getInstance("MD5"); + messagedigest.update(bytes); + md5 = bufferToHex(messagedigest.digest()); + + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + return md5; + } + + + private static String bufferToHex(byte bytes[]) { + if (bytes == null) { + return null; + } + return bufferToHex(bytes, 0, bytes.length); + } + + private static String bufferToHex(byte bytes[], int start, int len) { + if (bytes == null || start < 0 || len < 0) { + return null; + } + StringBuilder stringBuilder = new StringBuilder(2 * len); + int max = start + len; + for (int i = start; i < max; i++) { + String hex = Integer.toHexString(0xff & bytes[i]); + if (hex.length() == 1) { + hex = "0" + hex; + } + stringBuilder.append(hex); + } + return stringBuilder.toString(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/RC4Utils.java b/library/src/main/java/com/chwl/library/utils/codec/RC4Utils.java new file mode 100644 index 0000000..57847d5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/RC4Utils.java @@ -0,0 +1,105 @@ +package com.chwl.library.utils.codec; + +/** + * Created by Zhanghuiping on 14/6/9. + */ + +public class RC4Utils { + + public static String encrypt(String s, String key) throws Exception { + if (s == null || s.length() == 0 || key == null || key.length() == 0) { + return null; + } + return encrypt(s.getBytes(), key); + } + + public static String encrypt(byte[] input, String key) throws Exception { + if (input == null || input.length == 0 || key == null || key.length() == 0) { + return null; + } + RC4 encryptHelper = new RC4(key.getBytes()); + encryptHelper.doFinal(input); + return Base64Utils.encodeToString(input, Base64Utils.NO_WRAP); + } + + public static String decrypt(String s, String key) throws Exception { + if (s == null || s.length() == 0 || key == null || key.length() == 0) { + return null; + } + return decrypt(s.getBytes(), key); + } + + public static String decrypt(byte[] input, String key) throws Exception { + if (input == null || input.length == 0 || key == null || key.length() == 0) { + return null; + } + RC4 decryptHelper = new RC4(key.getBytes()); + input = Base64Utils.decode(input, Base64Utils.NO_WRAP); + decryptHelper.doFinal(input); + return new String(input); + } + + public static class RC4 { + private final static int STATE_LENGTH = 256; + private byte[] engineState = null; + private int x = 0; + private int y = 0; + private byte[] workingKey = null; + + public RC4(byte[] key) { + this.setKey(key); + } + + public void setKey(byte[] key) { + if (key == null || key.length == 0) { + return; + } + workingKey = new byte[key.length]; + System.arraycopy(key, 0, workingKey, 0, key.length); + x = 0; + y = 0; + if (engineState == null) { + engineState = new byte[STATE_LENGTH]; + } + // reset the state of the engine + for (int i = 0; i < STATE_LENGTH; i++) { + engineState[i] = (byte) i; + } + int i1 = 0; + int i2 = 0; + for (int i = 0; i < STATE_LENGTH; i++) { + i2 = ((key[i1] & 0xff) + engineState[i] + i2) & 0xff; + // do the byte-swap inline + byte tmp = engineState[i]; + engineState[i] = engineState[i2]; + engineState[i2] = tmp; + i1 = (i1 + 1) % key.length; + } + } + + public void doFinal(byte[] input) { + doFinal(input, 0, input.length); + } + + private void doFinal(byte[] input, int start, int count) { + if (input == null || start < 0 || count < 0) { + return; + } + for (int i = start; i < start + count; i++) { + x = (x + 1) & 0xff; + y = (engineState[x] + y) & 0xff; + // swap + byte tmp = engineState[x]; + engineState[x] = engineState[y]; + engineState[y] = tmp; + // xor + input[i] = (byte) (input[i] ^ engineState[(engineState[x] + engineState[y]) & 0xff]); + } + } + + public void reset() { + this.setKey(workingKey); + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/codec/SHAUtils.java b/library/src/main/java/com/chwl/library/utils/codec/SHAUtils.java new file mode 100644 index 0000000..7ebdad3 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/codec/SHAUtils.java @@ -0,0 +1,66 @@ +package com.chwl.library.utils.codec; + +import com.chwl.library.utils.log.MLog; + +import java.security.MessageDigest; + +public class SHAUtils { + + private static final String TAG = "SHAUtils"; + + private static String digest(String s, String algorithm) { + if (s == null || s.length() == 0 || algorithm == null || algorithm.length() == 0) { + return null; + } + String sha = null; + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + digest.update(s.getBytes("UTF-8")); + sha = bytesToHexString(digest.digest()); + } catch (Exception e) { + MLog.error(TAG, "digest error! " + e.toString()); + } + return sha; + } + + public static String getSHA1(String s) { + return digest(s, "SHA-1"); + } + + public static String getSHA256(String s) { + return digest(s, "SHA-256"); + } + + public static String getSHA384(String s) { + return digest(s, "SHA-384"); + } + + public static String getSHA512(String s) { + return digest(s, "SHA-512"); + } + + public static String getSHA(String s, String algorithm) { + return digest(s, algorithm); + } + + /** + * byte数组转为hex字符串 + * + * @param bytes + * @return + */ + private static String bytesToHexString(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/config/BasicConfig.java b/library/src/main/java/com/chwl/library/utils/config/BasicConfig.java new file mode 100644 index 0000000..c45c7c1 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/config/BasicConfig.java @@ -0,0 +1,285 @@ +package com.chwl.library.utils.config; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import com.chwl.library.utils.cache.CacheClientFactory; +import com.chwl.library.utils.file.StorageUtils; +import com.chwl.library.utils.log.MLog; + +import java.io.File; + + +/** + * Created by xujiexing on 14-6-12. + */ +public enum BasicConfig { + INSTANCE; + + private Context mContext; + private boolean isDebuggable; + private boolean isTestMode; + private File mLogDir; + private File mAppLogDir; + private File mRoot; + private File mConfigDir; + private File mCacheDir; + private File mVoiceDir; + private String channel; + //apk的原始渠道,用于判断是不是头条,快手等渠道 + private String originalChannel; + /*是否是审查中*/ + private boolean isCheck; + + private File mImageDir; + + // only used by unit test + public boolean isTestMode() { + return isTestMode; + } + + public void setIsTestMode(boolean isTestMode) { + this.isTestMode = isTestMode; + } + + private boolean isDebugMode(Context context) { + boolean debuggable = false; + ApplicationInfo appInfo = null; + PackageManager packMgmr = context.getPackageManager(); + try { + appInfo = packMgmr.getApplicationInfo(context.getPackageName(), + PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + MLog.error(this, e); + } + if (appInfo != null) { + debuggable = (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) > 0; + } + MLog.verbose(this, "isDebugMode debuggable = %b", debuggable); + return debuggable; + } + + /** + * 获取本地软件版本号名称 + */ + public static String getLocalVersionName(Context ctx) { + String localVersion = ""; + try { + PackageInfo packageInfo = ctx.getApplicationContext() + .getPackageManager() + .getPackageInfo(ctx.getPackageName(), 0); + localVersion = packageInfo.versionName; + + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return localVersion; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public String getChannel() { + return channel; + } + + public String getOriginalChannel() { + return originalChannel; + } + + public void setOriginalChannel(String originalChannel) { + this.originalChannel = originalChannel; + } + + public boolean isByteDanceChannel() { + return !TextUtils.isEmpty(originalChannel) && originalChannel.contains("toutiao"); + } + + public boolean isKwaiChannel() { + return !TextUtils.isEmpty(originalChannel) && originalChannel.contains("kuaishou"); + } + + /** + * @return Application context + */ + public Context getAppContext() { + return mContext; + } + + public void setAppContext(Context context) { + mContext = context; + setDebuggable(isDebugMode(context)); + } + + public boolean isDebuggable() { + return isDebuggable; + } + + public void setDebuggable(boolean debuggable) { + isDebuggable = debuggable; + } + + public void registerPrivateCacheClient(String uid) { + CacheClientFactory.registerPrivate(uid); + } + + public void removePrivateCacheClient() { + CacheClientFactory.removePrivate(); + } + + public File getRootDir() { + return this.mRoot; + } + + public File getExternalRootDir(String rootDir) { + File f = StorageUtils.getOwnCacheDirectory(mContext, rootDir); + if (f != null && !f.exists()) { + f.mkdirs(); + } + return f; + } + + public void setRootDir(String rootDir) { + File f = StorageUtils.getCacheDirectory(mContext, rootDir); + if (f != null && !f.exists()) { + f.mkdirs(); + } + this.mRoot = f; + } + + public File getConfigDir() { + return mConfigDir; + } + + /** + * 设置config的目录 + * + * @param dir + */ + public void setConfigDir(String dir) { + try { + mConfigDir = StorageUtils.getCacheDirectory(mContext, dir); + if (!mConfigDir.exists()) { + if (!mConfigDir.mkdirs()) { + MLog.error(this, "Can't create config dir " + mConfigDir); + return; + } + } + } catch (Exception e) { + MLog.error(this, "Set config dir error", e); + } + } + + public File getLogDir() { + return mLogDir; + } + + /** + * 设置log的目录 + * + * @param dir + */ + public void setLogDir(String dir) { + try { + mAppLogDir = setAppLogDir(dir); + mLogDir = StorageUtils.getCacheDirectory(mContext, dir); + if (!mLogDir.exists()) { + if (!mLogDir.mkdirs()) { + MLog.error(this, "Can't create log dir " + mLogDir); + return; + } + } + + } catch (Exception e) { + MLog.error(this, "Set log dir error", e); + } + } + + public File getAppLogDir() { + return mAppLogDir; + } + + private File setAppLogDir(String dir) { + File file = null; + try { + file = StorageUtils.getExternalStorageDirectory(mContext, dir); + if (mLogDir != null && !mLogDir.exists()) { + if (!mLogDir.mkdirs()) { + MLog.error(this, "Can't create app log dir " + mLogDir); + return null; + } + } + } catch (Exception e) { + MLog.error(this, "Set app log dir error", e); + } + return file; + } + + public File getCacheDir() { + return mCacheDir; + } + + public void setCacheDir(String dir) { + try { + mCacheDir = StorageUtils.getCacheDirectory(mContext, dir); + if (!mCacheDir.exists()) { + if (!mCacheDir.mkdirs()) { + MLog.error(this, "Can't create log dir " + mCacheDir); + return; + } + } + } catch (Exception e) { + MLog.error(this, "Set log dir error", e); + } + } + + public void setVoiceDir(String dir) { + try { + mVoiceDir = StorageUtils.getCacheDirectory(mContext, dir); + if (!mVoiceDir.exists()) { + if (!mVoiceDir.mkdirs()) { + MLog.error(this, "Can't create voice dir " + mVoiceDir); + return; + } + } + } catch (Exception e) { + MLog.error(this, "Set log voice error", e); + } + } + + public File getVoiceDir() { + return mVoiceDir; + } + + public boolean isCheck() { + return isCheck; + } + + public void setCheck(boolean check) { + isCheck = check; + } + + public void setImageDir(String imageDir) { + try { + mImageDir = StorageUtils.getExternalStorageDirectory(mContext, imageDir); + if (!mImageDir.exists()) { + if (!mImageDir.mkdirs()) { + MLog.error(this, "Can't create voice dir " + mImageDir); + return; + } + } + } catch (Exception e) { + MLog.error(this, "Set log voice error", e); + } + } + + public File getImageDir() { + return mImageDir; + } + + +} diff --git a/library/src/main/java/com/chwl/library/utils/constant/PackageNameConstants.kt b/library/src/main/java/com/chwl/library/utils/constant/PackageNameConstants.kt new file mode 100644 index 0000000..8c67406 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/constant/PackageNameConstants.kt @@ -0,0 +1,12 @@ +package com.chwl.library.utils.constant + +/** + * author: wushaocheng + * time: 2022/09/28 + * desc: 应用包名 + */ +object PackageNameConstants { + const val FACEBOOK_NAME = "com.facebook.katana"//facebook + const val LINE_NAME = "jp.naver.line.android"//line + const val GOOGLE_NAME = "com.android.vending"//google +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/file/BasicFileUtils.java b/library/src/main/java/com/chwl/library/utils/file/BasicFileUtils.java new file mode 100644 index 0000000..aeebcee --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/BasicFileUtils.java @@ -0,0 +1,513 @@ +package com.chwl.library.utils.file; + +import android.content.Context; +import android.media.MediaScannerConnection; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; + +import com.chwl.library.R; +import com.chwl.library.utils.FP; +import com.chwl.library.utils.ResUtil; +import com.chwl.library.utils.log.MLog; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BasicFileUtils { + + public static boolean isSDCardMounted() { + return availableMemInSDcard(); + } + + public static boolean availableMemInSDcard() { + if (!externalStorageExist()) { + return false; + } + File sdcard = Environment.getExternalStorageDirectory(); + StatFs statFs = new StatFs(sdcard.getPath()); + long blockSize = statFs.getBlockSize(); + long avaliableBlocks = statFs.getAvailableBlocks(); + long total = avaliableBlocks * blockSize / 1024; + if (total < 10) { + return false; + } + return true; + } + + public static boolean externalStorageExist() { + boolean ret = false; + ret = Environment.getExternalStorageState().equalsIgnoreCase( + Environment.MEDIA_MOUNTED); + return ret; + } + + /*public static String getRootDir() { + return Environment.getExternalStorageDirectory().getPath() + File.separator + + BasicConfig.getExternalFolderName(); + }*/ + + public static String getFileExt(String fileName) { + final int pos = fileName.lastIndexOf("."); + return pos == -1 ? "" : fileName.toLowerCase().substring(pos); + } + + public static String getFileName(String filePath) { + if (filePath != null) { + final String slash = "/"; + final int pos = filePath.lastIndexOf(slash) + 1; + if (pos > 0) { + return filePath.substring(pos); + } + } + return null; + } + + public static final String ZIP_EXT = ".zip"; + public static final String JPG_EXT = ".jpg"; + public static final String SPEEX_EXT = ".aud"; + + private static Map FILE_MIMES = new HashMap(); + + static { + FILE_MIMES.put(ZIP_EXT, "application/zip"); + FILE_MIMES.put(".bmp", "image/bmp"); + FILE_MIMES.put(".gif", "image/gif"); + FILE_MIMES.put(".jpe", "image/jpeg"); + FILE_MIMES.put(".jpeg", "image/jpeg"); + FILE_MIMES.put(JPG_EXT, "image/jpeg"); + FILE_MIMES.put(".png", "image/png"); + FILE_MIMES.put(".speex", "audio/speex"); + FILE_MIMES.put(".spx", "audio/speex"); + FILE_MIMES.put(SPEEX_EXT, "audio/speex"); + } + + public static String getFileMime(String fileName) { + String mime = FILE_MIMES.get(getFileExt(fileName)); + if (mime != null) { + return mime; + } + return "*/*"; + } + + public static void ensureDirExists(String dirPath) { + File dirFile = new File(dirPath); + if (!dirFile.exists()) { + dirFile.mkdirs(); + } + } + + public static void createDir(String dirPath, boolean nomedia) { + ensureDirExists(dirPath); + if (nomedia) { + File nomediafile = new File(dirPath + "/.nomedia"); + try { + nomediafile.createNewFile(); + } catch (IOException e) { + } + } + } + + public static File createFileOnSD(String dir, String name) { + File file = null; + if (isSDCardMounted()) { + createDir(dir, true); + String path = dir + File.separator + name; + file = new File(path); + try { + if (!file.exists() && !file.createNewFile()) { + file = null; + } + } catch (IOException e) { + //YLog.error("JXFileUtils", "can not create file on SD card, path = " + path); + file = null; + } + } + return file; + } + + public static boolean isFileExisted(String filePath) { + if (FP.empty(filePath)) { + return false; + } + try { + File file = new File(filePath); + return (file.exists() && file.length() > 0); + } catch (Exception e) { + return false; + } + } + + public static String getDirOfFilePath(String filePath) { + if (FP.empty(filePath)) { + return null; + } + int sepPos = filePath.lastIndexOf(File.separatorChar); + if (sepPos == -1) { + return null; + } + return filePath.substring(0, sepPos); + } + + public static void renameFile(String oldFile, String newFile) { + try { + File file = new File(oldFile); + file.renameTo(new File(newFile)); + } catch (Exception e) { + //YLog.error("JXFileUtils", "renameFile fail, oldFile = %s, %s", oldFile, e); + } + } + + public static void removeFile(String filename) { + if (!FP.empty(filename)) { + try { + File file = new File(filename); + file.delete(); + } catch (Exception e) { + } + } + } + + public static void removeDir(String dirPath) { + File dir = new File(dirPath); + if (dir.isDirectory()) { + File[] fileList = dir.listFiles(); + if (fileList != null && fileList.length > 0) { + for (File file : fileList) { + file.delete(); + } + } + } + dir.delete(); + } + + /** + * different from removeDir(path), this is a recursive ver. And it's silent + * if fname doesn't exist. + */ + public static void rm(String fname) { + rm(new File(fname)); + } + + public static void rm(File f) { + if (f.exists()) { + if (f.isDirectory()) + for (File i : FP.ref(f.listFiles())) + rm(i); + else + f.delete(); + } + } + + private static final int MAX_BUFF_SIZE = 1024 * 1024; + private static final int MIN_BUFF_SIZE = 4096; + + public static void copyFile(File src, File des) throws IOException { + if (des.exists()) { + des.delete(); + } + des.createNewFile(); + + FileInputStream in = new FileInputStream(src); + int length = in.available(); + if (length == 0) { + length = MIN_BUFF_SIZE; + } else if (length >= MAX_BUFF_SIZE) { + length = MAX_BUFF_SIZE; + } + FileOutputStream out = new FileOutputStream(des); + byte[] buffer = new byte[length]; + while (true) { + int ins = in.read(buffer); + if (ins == -1) { + in.close(); + out.flush(); + out.close(); + return; + } else { + out.write(buffer, 0, ins); + } + } + } + + public static boolean copyFile(String inFileName, String outFileName) { + try { + copyFile(new File(inFileName), new File(outFileName)); + return true; + } catch (Exception e) { + //YLog.error("JXFileUtils", "lcy copy file failed: %s", e); + return false; + } + } + + private static final int SCAN_MAX_RECURSION_HEIGHT = 12;//扫描文件递归最大层数 + public static final float FILE_TOTAL_WEIGHT = 100.0f;//文件总共权重 + + /** + * 扫描文件 每扫描到一个文件就回调一次 + * + * @param callback + */ + public static void scanFileSystem(ScannedFileCallback callback) { + WeakReference weakReference = new WeakReference(callback); + List sdCards = StorageUtils.findAllSdCardPaths(true); + if (sdCards != null) { + int size = sdCards.size(); + for (int i = 0; i < size; i++) { + scanFile(new File(sdCards.get(i)), FILE_TOTAL_WEIGHT / size, 0, weakReference); + } + } + } + + /** + * 递归遍历扫描文件 + * + * @param directories + * @param weight 这一层目录,在整个文件中所占比重 + * (总共 100.0 #{FILE_TOTAL_WEIGHT},每一层目录中,每个子目录或子文件评分这个比重权值) + * @param height 遍历递归的深度 + */ + private static void scanFile(File directories, float weight, int height, WeakReference callback) { + if (directories == null) { + return; + } + + if (height > SCAN_MAX_RECURSION_HEIGHT) { + MLog.warn("sqr", ResUtil.getString(R.string.utils_file_basicfileutils_01)); + return; + } else { + height++; + } + + try { + File[] fileList = directories.listFiles(); + if (fileList != null) { + int fileCount = fileList.length;//文件个数 + for (int i = 0; i < fileCount; i++) { + + if (fileIsDirectory(fileList[i])) { + scanFile(fileList[i], weight / fileCount, height, callback); + + } else if (fileIsStandardFile(fileList[i])) { + + if (callback != null && callback.get() != null) { + ScannedFileCallback mCallback = callback.get(); + if (mCallback != null) { + mCallback.onScanned(fileList[i].getAbsolutePath(), weight / fileCount); + } + } + } else { + MLog.warn("sqr", directories.getAbsolutePath() + + ResUtil.getString(R.string.utils_file_basicfileutils_02) + + fileList[i] == null ? " null" : fileList[i].getAbsolutePath()); + } + } + } else { + MLog.warn("sqr", directories.getAbsolutePath() + ResUtil.getString(R.string.utils_file_basicfileutils_03)); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private static boolean fileValid(File file) { + return file != null && file.exists(); + } + + private static boolean fileIsDirectory(File file) { + if (fileValid(file)) { + return file.isDirectory(); + } else { + return false; + } + } + + private static boolean fileIsStandardFile(File file) { + if (fileValid(file)) { + return file.isFile(); + } else { + return false; + } + } + + public interface ScannedFileCallback { + /** + * 扫描到文件即回调一次 + * + * @param filePath 文件的路径 + * @param weightOfTotalFile 扫描到的文件在整个文件系统占据的比重 + */ + public void onScanned(String filePath, float weightOfTotalFile); + } + + public static void addMp3Media(Context context, String filePath) { + addMp3Media(context, filePath, null); + } + + public static void addMp3Media(Context context, String filePath, + MediaScannerConnection.OnScanCompletedListener callback) { + MediaScannerConnection.scanFile(context, new String[]{filePath}, new String[]{".mp3"}, callback); + } + + /** + * 获取文件系统总共存储空间大小 + * @return 返回文件存储大小(byte) + */ + public static long getTotalSize(String filePath) { + StatFs statFs = new StatFs(filePath); + long blockSize, totalCount; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + blockSize = statFs.getBlockSizeLong(); + totalCount = statFs.getBlockCountLong(); + } else { + blockSize = statFs.getBlockSize(); + totalCount = statFs.getBlockCount(); + } + return totalCount * blockSize; + } + + /** + * 获取文件可用存储空间大小 + * @param dir 目录文件 + * @return 返回目录文件可用存储大小(byte) + */ + public static long getRemainSize(File dir) { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return 0l; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return dir.getUsableSpace(); + } + final StatFs stats = new StatFs(dir.getPath()); + return stats.getBlockSize() * stats.getAvailableBlocks(); + } + + /** + * 移动文件(通常用在不同SD卡间复制文件,相同SD卡间复制使用renameTo更快) + * + * @param srcFileName 源文件完整路径 + * @param desDirName 目的目录完整路径 + * + * @return 文件移动成功返回true,否则返回false + */ + public static void moveFile(String srcFileName, String desDirName) throws CustomFileException{ + File srcFile = new File(srcFileName); + if (!srcFile.exists() || !srcFile.isFile()) { + return; + } + BasicFileUtils.ensureDirExists(desDirName); + File desFile = new File(desDirName + File.separator + srcFile.getName()); + + FileChannel inChannel = null, outChannel = null; + try { + inChannel = new FileInputStream(srcFile).getChannel(); + outChannel = new FileOutputStream(desFile).getChannel(); + inChannel.transferTo(0, inChannel.size(), outChannel); + } catch (IOException e) { + throw new TargetNotPreparedException(); + } finally { + try { + if (inChannel != null) { + inChannel.close(); + } + if (outChannel != null) { + outChannel.close(); + } + } catch (IOException e) { + } + } + } + + /** + * 扫描文件 每扫描到一个文件就回调一次 + * + * @param callback + */ + public static void moveDirectory(String srcDirName, String desDirName, int recursionHeight,ScannedFileCallback callback) throws CustomFileException { + WeakReference weakReference = new WeakReference(callback); + moveDirectory(srcDirName, desDirName, recursionHeight, FILE_TOTAL_WEIGHT, weakReference); + } + + /** + * 移动目录 + * + * @param srcDirName 源目录完整路径 + * @param desDirName 目的目录完整路径 + * @param recursionHeight 目录可递归的最大深度 + * @param weight 占权重 + * + * @return 目录移动成功返回true,否则返回false + */ + public static boolean moveDirectory(String srcDirName, String desDirName, int recursionHeight, float weight, WeakReference callback) throws CustomFileException { + File srcDir = new File(srcDirName); + if (!srcDir.exists() || !srcDir.isDirectory()) { + return false; + } + + File desDir = new File(desDirName); + if (!desDir.exists()) { + desDir.mkdirs(); + } + + //存储空间不够 + if (getRemainSize(desDir) < JXFileUtils.getFileSize(srcDir)) { + throw new FileNoSpaceException(); + } + + File[] sourceFiles = srcDir.listFiles(); + if (sourceFiles == null) { + if (callback != null && callback.get() != null) { + ScannedFileCallback mCallback = callback.get(); + if (mCallback != null) { + mCallback.onScanned(srcDir.getAbsolutePath(), weight); + } + } + } else { + for (File sourceFile : sourceFiles) { + if (sourceFile.isFile()) { + moveFile(sourceFile.getAbsolutePath(), desDir.getAbsolutePath()); + + if (callback != null && callback.get() != null) { + ScannedFileCallback mCallback = callback.get(); + if (mCallback != null) { + mCallback.onScanned(sourceFile.getAbsolutePath(), weight / sourceFiles.length); + } + } + } else if (sourceFile.isDirectory() && recursionHeight > 0) { + moveDirectory(sourceFile.getAbsolutePath(), desDir.getAbsolutePath() + File.separator + sourceFile.getName(), --recursionHeight, weight / sourceFiles.length, callback); + } + } + } + return deleteFile(srcDir); + } + + /** + * 删除文件或目录 + * @param file + * @return + */ + public static boolean deleteFile(File file) { + if (file == null || !file.exists()) { + return false; + } + if (file.isFile()) { + file.delete(); + } + File[] sourceFiles = file.listFiles(); + for (int i = sourceFiles.length - 1; i >= 0 ; i--) { + if (sourceFiles[i].isDirectory()) { + deleteFile(sourceFiles[i]); + } + sourceFiles[i].delete(); + } + return true; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/file/CMDExecute.java b/library/src/main/java/com/chwl/library/utils/file/CMDExecute.java new file mode 100644 index 0000000..8e843fb --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/CMDExecute.java @@ -0,0 +1,39 @@ + +package com.chwl.library.utils.file; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +public class CMDExecute { + + /** + * + * @param cmd 命令存放参数 + * @param workdirectory + * @return + * @throws IOException + */ + public synchronized String run(String[] cmd, String workdirectory) throws IOException { + String result = ""; + try { + ProcessBuilder builder = new ProcessBuilder(cmd); + if (workdirectory != null) + builder.directory(new File(workdirectory)); + builder.redirectErrorStream(true); + Process process = builder.start(); + InputStream in = process.getInputStream(); + byte[] buffer = new byte[1024]; + int ret = in.read(buffer); + while (ret != -1) { + String temp = new String(buffer, 0, ret); + result = result + temp; + ret = in.read(buffer); + } + in.close(); + } catch (Exception ex) { + ex.printStackTrace(); + } + return result; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/file/CustomFileException.java b/library/src/main/java/com/chwl/library/utils/file/CustomFileException.java new file mode 100644 index 0000000..1591240 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/CustomFileException.java @@ -0,0 +1,11 @@ +package com.chwl.library.utils.file; + +/** + * Creator: 舒强睿 + * Date:2015/6/24 + * Time:18:37 + *

+ * Description:操作文件的自定义异常 + */ +public class CustomFileException extends Exception { +} diff --git a/library/src/main/java/com/chwl/library/utils/file/FileNoSpaceException.java b/library/src/main/java/com/chwl/library/utils/file/FileNoSpaceException.java new file mode 100644 index 0000000..c7ce52d --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/FileNoSpaceException.java @@ -0,0 +1,11 @@ +package com.chwl.library.utils.file; + +/** + * Creator: 舒强睿 + * Date:2015/6/24 + * Time:18:39 + *

+ * Description:文件空间不足 + */ +public class FileNoSpaceException extends CustomFileException { +} diff --git a/library/src/main/java/com/chwl/library/utils/file/JXFileUtils.java b/library/src/main/java/com/chwl/library/utils/file/JXFileUtils.java new file mode 100644 index 0000000..6ffb351 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/JXFileUtils.java @@ -0,0 +1,1046 @@ +package com.chwl.library.utils.file; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Pair; + +import com.chwl.library.utils.BlankUtil; +import com.chwl.library.utils.FP; +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.codec.MD5Utils; +import com.chwl.library.utils.config.BasicConfig; +import com.chwl.library.utils.log.MLog; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +public class JXFileUtils extends BasicFileUtils { + public static final String TEMP_DIR = "temp"; + private static final String ROOT_DIR = "/erdmusic"; + private static final String DOWNLOAD_DIR = "/download"; + private static final String ATTR_DIR = "/attributes"; + private static final String IMAGES_DIR = "/images"; + private static final String SONGS_DIR = "/songs"; + private static final String UPDATE_DIR = "/update"; + private static final String ACT_RECOMMAND_FILE = "temp_act_recomm.txt"; + private static final String RECORD_EXT_HIGH_CPU = ".aac"; + private static final String RECORD_EXT_LOW_CPU = ".wav"; + private static final String RECORD_PUBLISH_EXT = ".m4a"; + private static final String MUSIC_CACHE = "/cacheMusic"; + + private static final int MIN_LEN_OF_VALID_WAV = 128 * 1024; + private static final int MIN_LEN_OF_VALID_AAC = 8 * 1024; + + static final String[] AUDIO_EXTS = new String[] { + RECORD_EXT_HIGH_CPU, RECORD_EXT_LOW_CPU, RECORD_PUBLISH_EXT, ".rec", + ".mp4", ".rec2" }; + + private FileOutputStream mFileOutputStream = null; + private BufferedOutputStream mBufferedOutputStream = null; + private File mFile; + + /* + *是否是有效音频文件 + */ + public static boolean isValidAudioFile(String path) { + if (!BlankUtil.isBlank(path)) { + String ext = JXFileUtils.getFileExtension(path); + if (!BlankUtil.isBlank(ext)) { + for (String extItem : AUDIO_EXTS) { + if (ext.equalsIgnoreCase(extItem)) { + return true; + } + } + } + } + return false; + } + + public static String getPkgDir(Context context) { + return context.getFilesDir().getAbsolutePath(); + } + /* + *读取txt文件内容 + */ + public static String getTxtFileContent(Context context,String fileName) { + String path = fileName; + String content = ""; + if (BlankUtil.isBlank(fileName)) { + return content; + } + File file = new File(path); + if (file.isFile()) { + InputStream instream = null; + try { + if (fileName.startsWith(context.getFilesDir().getPath())) { + instream = context.openFileInput(JXFileUtils + .getFileName(fileName)); + } else { + instream = new FileInputStream(file); + } + if (instream != null) { + InputStreamReader inputreader = new InputStreamReader( + instream); + BufferedReader buffreader = new BufferedReader(inputreader); + String line; + StringBuffer contentBuffer = new StringBuffer(); + while ((line = buffreader.readLine()) != null) { + contentBuffer.append(line).append("\n"); + } + content = contentBuffer.toString(); + buffreader.close(); + } + } catch (Exception e) { + //YLog.error("getTxtFileContent", "read fail, e = " + e); + } finally { + if (instream != null) { + try { + instream.close(); + } catch (Exception e) { + } + } + } + } + return content; + } + /* + *返回文件扩展名 + */ + public static String getFileExtension(String filePath) { + String fileName = getFileName(filePath); + int index = fileName.lastIndexOf("."); + if (index != -1) { + return fileName.substring(index); + } + return null; + } + /* + *获取文件名 + */ + public static String getFileName(String filePath) { + if (filePath != null) { + final String slash = File.separator; + final int pos = filePath.lastIndexOf(slash) + 1; + return filePath.substring(pos); + } + return null; + } + + /* drop the extension of a filename */ + public static String dropExt(String fname) { + if (!FP.empty(fname)) { + int pos = fname.lastIndexOf("."); + if (pos != -1) + return FP.take(pos, fname); + } + return fname; + } + + /** + * 判断文件是否存在 + * @param filePath + * @return + */ + public static boolean isFileExisted(String filePath) { + if (BlankUtil.isBlank(filePath)) { + return false; + } + try { + File file = new File(filePath); + return (file.exists() && file.length() > 0); + } catch (Exception e) { + return false; + } + } + + /** + * 重命名文件 + * @param oldFile + * @param newFile + */ + public static void renameFile(String oldFile, String newFile) { + try { + File file = new File(oldFile); + file.renameTo(new File(newFile)); + } catch (Exception e) { + //YLog.error("JXFileUtils", "renameFile fail, oldFile = %s, %s", oldFile, e); + } + } + //移除多个文件 + public static void removeFiles(List> fileNames) { + for (Pair p : fileNames) { + if (p.second != null) { + removeFile(p.second); + } + } + } + //移除一个文件 + public static void removeFile(String filename) { + if (!BlankUtil.isBlank(filename)) { + try { + File file = new File(filename); + file.delete(); + } catch (Exception e) { + } + } + } + //移除目录 + public static void removeDir(String dirPath) { + File dir = new File(dirPath); + if (dir.isDirectory()) { + File[] fileList = dir.listFiles(); + if (fileList != null && fileList.length > 0) { + for (File file : fileList) { + file.delete(); + } + } + } + dir.delete(); + } + + /** + * different from removeDir(path), this is a recursive ver. And it's silent + * if fname doesn't exist. + */ + public static void rm(String fname) { + rm(new File(fname)); + } + + public static void rm(File f) { + if (f.exists()) { + if (f.isDirectory()) + for (File i : FP.ref(f.listFiles())) + rm(i); + else + f.delete(); + } + } + + /*public static File getFileFromURL(String url) { + if (BlankUtil.isBlank(url)) { + return null; + } + int idx = url.lastIndexOf(File.separatorChar); + return new File(getYYImImageDir() + url.substring(idx + 1)); + }*/ + + public static File getFileFromURL(String base, String url) { + if (BlankUtil.isBlank(url)) { + return null; + } + int idx = url.lastIndexOf(File.separatorChar); + return new File(base, url.substring(idx + 1)); + } + + public static String getImageFilePathFromUri(Context context, Uri uri) { + if(uri == null){ + //YLog.debug("xuwakao", "getFilePathFromUri param uri == NULL"); + return null; + } + + File file = new File(uri.getPath()); + if (file.isFile()) { + return file.getPath(); + } + if ("file".equals(uri.getScheme())) { + String ret = uri.toString().substring(7); + ret = decodeUri(ret); + return ret; + } else if ("content".equals(uri.getScheme())){ + ContentResolver cr = context.getContentResolver(); + Cursor cursor = cr.query(uri, new String[] { MediaStore.Images.ImageColumns.DATA }, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int index = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA); + String ret = cursor.getString(index); + //YLog.verbose("xuwakao" , "getFilePathFromUri ret = " + ret + ", index = " + index + ", cursor = " + cursor); + ret = decodeUri(ret); + cursor.close(); + return ret; + } + } + return null; + } + + public static String decodeUri(String uri) { + if (BlankUtil.isBlank(uri)) { + return uri; + } + int index = uri.indexOf('%'); + if (index != -1) { + uri = Uri.decode(uri); + } + return uri; + } + + public static boolean isSDCardMounted() { + return availableMemInSDcard(); + } + + public static boolean externalStorageExist() { + boolean ret = false; + ret = Environment.getExternalStorageState().equalsIgnoreCase( + Environment.MEDIA_MOUNTED); + return ret; + } + + public static boolean checkFileValidation(String filepath, String md5) throws IOException { + final String fileMd5 = MD5Utils.getFileMd5String(filepath); + if(fileMd5==null){ + return false; + } + return fileMd5.equals(md5); + } + + /** + * 获取一个临时文件 + * @param context + * @param uniqueName + * @return + */ + public static File getTempFile(Context context, String uniqueName){ + String tempPath = BasicConfig.INSTANCE.getRootDir().getAbsolutePath()+File.separator+TEMP_DIR; + File tmpFile = new File(tempPath); + if(!tmpFile.exists()) + tmpFile.mkdirs(); + return new File(tmpFile.getAbsolutePath() + File.separator + uniqueName); + } + + + /** + * 保存图片到 PNG文件 + * @param bitmap + * @param filePath + */ + public static void saveBitmapToPNG(Bitmap bitmap, String filePath)throws Exception{ + saveBitmap(bitmap,filePath, CompressFormat.PNG); + } + + /** + * 保存图片到 JPG文件 + * @param bitmap + * @param filePath + */ + public static void saveBitmapToJPG(Bitmap bitmap, String filePath)throws Exception{ + saveBitmap(bitmap,filePath, CompressFormat.JPEG); + } + + /** + * 保存图片到文件 + * @param bitmap + * @param filePath + * @param format + * @throws Exception + */ + public static void saveBitmap(Bitmap bitmap, String filePath,CompressFormat format)throws Exception{ + if (bitmap == null) { + return; + } + + if(format == null){ + format = CompressFormat.PNG; + } + File barcodeFile = new File(filePath); + if (!barcodeFile.exists() ) { + barcodeFile.createNewFile(); + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(barcodeFile); + bitmap.compress(format, 90, fos); + } catch (Exception fnfe) { + MLog.error(TAG, "Couldn't access file %s due to %s", barcodeFile, fnfe); + throw fnfe; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ioe) { + // do nothing + } + } + } + } + + + + public static String dropPrefix(String s, String prefix) { + return s.startsWith(prefix) ? FP.drop(FP.length(prefix), s) : s; + } + + /** + * Safe concatenate paths no matter the first one ends with / or the second + * one starts with /. + */ + public static String concatPath(String p1, String p2) { + return p1.endsWith(File.separator) ? p1 + dropPrefix(p2, File.separator) : p1 + File.separator + + dropPrefix(p2, File.separator); + } + + public static String concatPaths(String... ss) { + String path = ""; + for (String s : ss) + path = concatPath(path, s); + return path; + } + + + + public static String getYYActRecommFilename(Context context) { + File file = context.getFileStreamPath(ACT_RECOMMAND_FILE); + return file.getPath(); + } + + + + public static String getPropsConfigNotifyFilePath(Context context,String url) { + String fileName = url; + int index = url.lastIndexOf(File.separatorChar); + if (index != -1) { + fileName = url.substring(index + 1); + } + File cacheDir = StorageUtils.getIndividualCacheDirectory(context, null); + File propsConfigFile = new File(cacheDir, fileName); + return propsConfigFile.getAbsolutePath(); + } + + + + public static JXFileUtils createFile(String path) throws Exception { + String dir = JXFileUtils.getDirOfFilePath(path); + String name = JXFileUtils.getFileName(path); + File f = createFileOnSD(dir, name); + return new JXFileUtils(f, null); + } + + + + public static JXFileUtils openFile(String filePath) throws Exception { + String dirPath = filePath.substring(0, filePath.lastIndexOf(File.separator)); + createDir(dirPath, true); + + File file = new File(filePath); + if (!file.exists() && !file.createNewFile()) { + file = null; + } + return new JXFileUtils(file, null); + } + + private JXFileUtils(File file, FileOutputStream fileos) throws Exception { + mFile = file; + mFileOutputStream = fileos; + if (mFile != null) { + if (mFileOutputStream == null) { + mFileOutputStream = new FileOutputStream(mFile); + } + mBufferedOutputStream = new BufferedOutputStream(mFileOutputStream); + } else { + throw new Exception( + "YYFileOutput, can not create file output stream"); + } + } + + /** + * Ensure the parent directory of given file path exists. make directories + * if need. + * + * @param filePath + * A file path. + * @return True for success, false otherwise. + */ + public static boolean ensureFileDirExists(String filePath) { + String dir = getDirOfFilePath(filePath); + if (BlankUtil.isBlank(dir)) { + return false; + } + ensureDirExists(dir); + return true; + } + + public static String getDirOfFilePath(String filePath) { + if (BlankUtil.isBlank(filePath)) { + return null; + } + int sepPos = filePath.lastIndexOf(File.separatorChar); + if (sepPos == -1) { + return null; + } + return filePath.substring(0, sepPos); + } + + + + public void write(Bitmap bmp) { + write(bmp, 80); + } + + public void write(Bitmap bmp, int compressRate) { + bmp.compress(CompressFormat.JPEG, compressRate, mBufferedOutputStream); + } + + public void writeYCbCr420SP(byte[] data, int width, int height) { + YuvImage image = new YuvImage(data, PixelFormat.YCbCr_420_SP, width, height, null); + image.compressToJpeg(new Rect(0, 0, width, height - 1), 100, mBufferedOutputStream); + } + + public void write(InputStream is) { + int bytes = 0; + byte[] buffer = new byte[4096]; + try { + while ((bytes = is.read(buffer)) != -1) { + mBufferedOutputStream.write(buffer, 0, bytes); + } + } catch (IOException e) { + //YLog.error(this, e); + } + } + + public void write(String fileName) { + try { + FileInputStream fis = new FileInputStream(fileName); + write(fis); + fis.close(); + } catch (Exception e) { + //YLog.error(this, e); + } + } + + public void write(byte[] buffer) { + try { + mBufferedOutputStream.write(buffer); + } catch (IOException e) { + //YLog.error(this, e); + } + } + + public void write(byte[] buffer, int offset, int length) { + try { + mBufferedOutputStream.write(buffer, offset, length); + } catch (IOException e) { + //YLog.error(this, e); + } + } + + public void close() { + try { + if (mBufferedOutputStream != null) { + mBufferedOutputStream.flush(); + mBufferedOutputStream.close(); + } + if (mFileOutputStream != null) { + mFileOutputStream.close(); + } + } catch (IOException e) { + //YLog.error(this, e); + } + } + + public File getFile() { + return mFile; + } + + public static long getFileLength(String file) { + File tmp = new File(file); + return tmp.length(); + } + + public static String fallbackFile(String file) { + String ext = getFileExt(file); + int i = file.lastIndexOf("."); + return i == -1 ? "" : file.substring(0, i + 1) + "bak" + ext; + } + + /** + * Check validity of record file, currently only .aac and .wav file are + * supported, check is based on the file length. + * + * @param filePath + * Must be end with .aac or .wav. + */ + public static boolean isValidRecordFile(String filePath) { + if (!isFileExisted(filePath)) { + return false; + } + + String ext = getFileExtension(filePath); + if (ext == null) { + return false; + } + + //YLog.verbose(JXFileUtils.class, "lcy file extension is %s", ext); + + boolean aac = false; + if (!(aac = ext.equalsIgnoreCase(RECORD_EXT_HIGH_CPU)) + && !ext.equalsIgnoreCase(RECORD_EXT_LOW_CPU)) { + //YLog.debug(JXFileUtils.class, "lcy record extension check failed."); + return false; + } + + final long len = JXFileUtils.getFileLength(filePath); + final long minLen = aac ? MIN_LEN_OF_VALID_AAC : MIN_LEN_OF_VALID_WAV; + boolean ret = (len >= minLen); + //YLog.debug(JXFileUtils.class, "lcy file length invalid %d, %d, %s.",len, minLen, ext); + return ret; + } + + /** + * Get file size, if it is a directory, will accumulate the size of the + * inner files recursively. + * + * @param file + * @return 0 if no file, the total size of the files. + */ + public static long getFileSize(File file) { + if (file == null) { + return 0l; + } + long size = 0; + File fileList[] = file.listFiles(); + for (int i = 0; i < fileList.length; i++) { + if (fileList[i].isDirectory()) { + size = size + getFileSize(fileList[i]); + } else { + // YLog.verbose("Simon", "file: " + fileList[i] + " size: " + + // fileList[i].length()); + size = size + fileList[i].length(); + } + } + return size; + } + + /** + * Get human readable file size. + * + * @param bytes + * Num of bytes. + */ + public static String getHumanReadableFileSize(long bytes) { + // less than 1K, show it in Bs, less than 1M, show it in KBs, otherwise + // show in MBs. + if (bytes < 1024) { + return getFileSizeInBytes(bytes); + } + return (bytes >> 20) == 0 ? getFileSizeInKBytes(bytes) + : getFileSizeInMBytes(bytes); + } + + public static String getFileSizeInBytes(long bytes) { + return String.format("%dB", bytes); + } + + public static String getFileSizeInKBytes(long bytes) { + long kbs = (bytes >> 10); + return String.format("%dK", kbs); + } + + public static String getFileSizeInMBytes(long bytes) { + float kbs = bytes / 1024.0f; + float mbs = kbs / 1024; + DecimalFormat df = new DecimalFormat("0.00M"); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + String ret = df.format(mbs); + return ret; + } + + private static final String BARCODE_FILE_EXT = ".png"; + private static final String YY_BARCODE_DIR = "YYBarcode"; + private static final String TAG = "JXFileUtils"; + private static final int MAX_FILENAME_LENGTH = 24; + private static final Pattern NOT_ALPHANUMERIC = Pattern + .compile("[^A-Za-z0-9]"); + + /** + * Save bitmap to external storage public directory for pictures. + * + * @param bitmap + * @param fileName + */ + public static void saveBitmapToPublicDir(Bitmap bitmap, String fileName) { + if (bitmap == null) { + return; + } + + File barcodesRoot = new File( + Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + YY_BARCODE_DIR); + + if (!barcodesRoot.exists() && !barcodesRoot.mkdirs()) { + //YLog.warn(TAG, "Couldn't make dir %s", barcodesRoot); + // showErrorMessage(R.string.msg_unmount_usb); + return; + } + + File barcodeFile = new File(barcodesRoot, makeFileName(fileName) + + BARCODE_FILE_EXT); + barcodeFile.delete(); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(barcodeFile); + bitmap.compress(CompressFormat.PNG, 0, fos); + } catch (FileNotFoundException fnfe) { + //YLog.warn(TAG, "Couldn't access file %s due to %s", barcodeFile,fnfe); + // showErrorMessage(R.string.msg_unmount_usb); + return; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ioe) { + // do nothing + } + } + } + + } + + private static String makeFileName(CharSequence contents) { + String fileName = NOT_ALPHANUMERIC.matcher(contents).replaceAll("_"); + if (fileName.length() > MAX_FILENAME_LENGTH) { + fileName = fileName.substring(0, MAX_FILENAME_LENGTH); + } + return fileName; + } + + /** + * Read file bytes and return. + * + * @param file + * Must not be null. + * @return null if input is not a valid file. + */ + public static byte[] fileToByteArray(File file) { + if (!file.exists() || !file.canRead()) { + return null; + } + + try { + return streamToBytes(new FileInputStream(file)); + } catch (FileNotFoundException e) { + //YLog.error(TAG, e); + return null; + } + } + + /** + * Convert input stream to byte array. + * + * @return null if failed. + */ + public static byte[] streamToBytes(InputStream inputStream) { + byte[] content = null; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BufferedInputStream bis = new BufferedInputStream(inputStream); + + try { + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = bis.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + + content = baos.toByteArray(); + if (content.length == 0) { + content = null; + } + + baos.close(); + bis.close(); + } catch (IOException e) { + //YLog.error(TAG, e); + } finally { + if (baos != null) { + try { + baos.close(); + } catch (IOException e) { + //YLog.error(TAG, e); + } + } + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + //YLog.error(TAG, e); + } + } + } + + return content; + } + + private static final int MAX_BUFF_SIZE = 1024 * 1024; + private static final int MIN_BUFF_SIZE = 4096; + + public static void copyFile(File src, File des) throws IOException { + if (des.exists()) { + des.delete(); + } + des.createNewFile(); + + FileInputStream in = new FileInputStream(src); + int length = in.available(); + if (length == 0) { + length = MIN_BUFF_SIZE; + } else if (length >= MAX_BUFF_SIZE) { + length = MAX_BUFF_SIZE; + } + FileOutputStream out = new FileOutputStream(des); + byte[] buffer = new byte[length]; + while (true) { + int ins = in.read(buffer); + if (ins == -1) { + in.close(); + out.flush(); + out.close(); + return; + } else { + out.write(buffer, 0, ins); + } + } + } + + public static boolean copyFile(String inFileName, String outFileName) { + try { + copyFile(new File(inFileName), new File(outFileName)); + return true; + } catch (Exception e) { + MLog.error("JXFileUtils", "lcy copy file failed: %s", e); + return false; + } + } + + + + public static String getImagePathFromURL(String basePath, String url) { + if (BlankUtil.isBlank(url)) { + return null; + } + int idx = url.lastIndexOf(File.separatorChar); + String path = url.substring(idx + 1); + return basePath + File.separator + path; + } + + /** A shortcut alias class for code golf */ + public static class IO { + public static void mkdir(String path) { + ensureDirExists(path); + } + + public static String concat(String p1, String p2) { + return concatPath(p1, p2); + } + + public static String concats(String... ps) { + return concatPaths(ps); + } + + /** + * Differ from isFileExisted, this func desn't test if the length is + * zero. + */ + public static boolean exist(String f) { + if (!FP.empty(f)) + try { + return new File(f).exists(); + } catch (Exception e) { + } + return false; + } + + public static boolean touch(String f) { + if (exist(f)) + return false; + if (ensureFileDirExists(f)) + try { + new File(f).createNewFile(); + } catch (Exception e) { + } + return true; + } + } + + + public static void deleteOldFiles(String dirPath, final String postfix, long downloadTime) { + File dir = new File(dirPath); + if (dir.isDirectory()) { + FilenameFilter filter = new FilenameFilter() { + public boolean accept(File dir, String filename) { + return (filename.toLowerCase().indexOf(postfix) != -1); + } + }; + List portraitList = new ArrayList(); + //TODO xianbing + //FIXME xianbing +// if (StringUtils.equal(dirPath, JXFileUtils.getYYImageDir(), true)) { +// List friendList = Content.obj(YYMobile.gContext).getDisplayFriends(true, true); +// for (UserInfo info : friendList) { +// if (!StringUtils.isNullOrEmpty(info.getPortraitUrl())) { +// portraitList.add(Utils.getPhotoFullPathFromUrl(info.getUid(), info.getPortraitUrl(), +// FriendPictureInfo.ImgType.SMALL)); +// } +// } +// } + File[] fileList = dir.listFiles(filter); + if (fileList != null && fileList.length > 0) { + long current = System.currentTimeMillis(); + for (File file : fileList) { + if (!portraitList.contains(file.getPath()) && current - file.lastModified() > downloadTime) { + file.delete(); + } + } + } + } + } + + public static boolean isSameFile(String path1, String path2) { + if (path1 == null || path2 == null) { + //YLog.error(JXFileUtils.class, "lcy input illegal for comparsion %s %s.", path1, path2); + return false; + } + return new File(path1).equals(new File(path2)); + } + + public static String getRootDir() { + return BasicConfig.INSTANCE.getRootDir().getAbsolutePath(); + } + + public static String getExternalDownloadDir(String rootDir, boolean primaryExt) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !primaryExt) { + return getSecondaryPrivatePackage() + DOWNLOAD_DIR; + } + return rootDir + ROOT_DIR + DOWNLOAD_DIR; + } + + public static String getYYTempDir() { + return getRootDir() + TEMP_DIR; + } + + public static String getYYImReceivedImageDir() { + return getRootDir() + IMAGES_DIR + File.separator; + } + + /** + * 歌曲配置文件目录 + * @return + */ + public static String getMusicAttrDir() { + String attrDir = Environment.getExternalStorageDirectory().getAbsolutePath() + ROOT_DIR + DOWNLOAD_DIR + ATTR_DIR + File.separator; + ensureDirExists(attrDir); + return attrDir; + } + + + /** + * 获取歌曲下载目录 + * @param rootDir 根目录路径 + * @param primaryExt true=内置存储卡,false=外置存储卡 + * @return + */ + public static String getMusicDownloadDir(String rootDir, boolean primaryExt) { + String path = getExternalDownloadDir(rootDir, primaryExt) + SONGS_DIR ; + File file = new File(path); + if (!file.exists()) { + file.mkdirs(); + } + return path.concat(File.separator); + } + + /** + * 获取图片下载路径 + * @return + */ + public static String getImageDownloadPath() { + String path = Environment.getExternalStorageDirectory().getPath() + ROOT_DIR + DOWNLOAD_DIR + IMAGES_DIR ; + File file = new File(path); + if (!file.exists()) { + file.mkdirs(); + } + return path; + } + + /** + * 获取歌曲缓存下载路径 + * @return + */ + public static String getMusicCacheDownloadPath() { + String path = Environment.getExternalStorageDirectory().getPath() + ROOT_DIR + DOWNLOAD_DIR + MUSIC_CACHE ; + File file = new File(path); + if (!file.exists()) { + file.mkdirs(); + } + return path; + } + + /** + * 获取外置存储卡私有目录 + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static File getSecondaryPrivatePackage() { + File[] file = BasicConfig.INSTANCE.getAppContext().getExternalFilesDirs(null); + if (file != null && file.length > 1) { + return file[1]; + } + return null; + } + + public static String getUpdateDir() { + String dirPath = getRootDir() + UPDATE_DIR + File.separator; + createDir(dirPath, true); + return dirPath; + } + + public static String getYYImageFileLocalPath(String name) { + if (FP.empty(name)) { + return name; + } + String filename = name; + int index = name.lastIndexOf(File.separatorChar); + if (index != -1) { + filename = name.substring(index + 1); + } + String dirPath = getYYImReceivedImageDir(); + createDir(dirPath, true); + return dirPath + filename; + } + + public static boolean isTempFile(Context c, String path) { + File temp = getTempFile(c, getFileName(path)); + return StringUtils.equals(path, temp.getPath()); + } + + public static String getLocalPathFromUrl(String url) { + String fileName = url.substring(url.lastIndexOf("/") + 1, url.length()); + return getYYImReceivedImageDir() + fileName; + } + + public static String getDefaultDownloadRootDir() { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/file/StorageUtils.java b/library/src/main/java/com/chwl/library/utils/file/StorageUtils.java new file mode 100644 index 0000000..3722385 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/StorageUtils.java @@ -0,0 +1,536 @@ +/******************************************************************************* + * Copyright 2011-2013 Sergey Tarasevich + * + * 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.chwl.library.utils.file; + +import static android.os.Environment.MEDIA_MOUNTED; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Environment; +import android.text.TextUtils; + +import com.chwl.library.R; +import com.chwl.library.common.file.FileHelper; +import com.chwl.library.utils.ResUtil; +import com.chwl.library.utils.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Provides application storage paths + * + * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) + * @since 1.0.0 + */ +public final class StorageUtils { + + public static final String DIR_LIVE = "live"; + public static final String DIR_PRIEVIEW = "preview"; + public static final String DIR_PHOTO_ALBUM = "photo"; + public static final String DIR_CHANNEL = "channel"; + public static final String DIR_HEAD = "head"; + public static final String DIR_GIFT = "gift"; + public static final String DIR_SPLASH = "splash"; + private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE"; + private static final String INDIVIDUAL_DIR_NAME = "images"; + + private StorageUtils() { + } + + /** + * Returns application cache directory. Cache directory will be created on SD card + * ("/Android/data/[app_package_name]/cache") if card is mounted and app has appropriate permission. Else - + * Android defines cache directory on device's file system. + * + * @param context Application context + * @return Cache {@link File directory} + */ + public static File getCacheDirectory(Context context, String uniqueName) { + File appCacheDir = null; + if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) { + appCacheDir = getExternalCacheDir(context); + } + if (appCacheDir == null) { + appCacheDir = context.getCacheDir(); + } + if (appCacheDir == null) { + //L.w("Can't define system cache directory! The app should be re-installed."); + } + + return null != uniqueName ? new File(appCacheDir, uniqueName) : appCacheDir; + } + + public static File getExternalStorageDirectory(Context context, String uniqueName) { + File appDir = null; + if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) { + appDir = FileHelper.getRootCacheDir(); + } + if (appDir == null) { + appDir = context.getCacheDir(); + } + + if (!TextUtils.isEmpty(uniqueName)) { + appDir = new File(appDir, uniqueName); + if (!appDir.exists()) { + appDir.mkdirs(); + } + } + return appDir; + } + + /** + * Returns individual application cache directory (for only image caching from ImageLoader). Cache directory will be + * created on SD card ("/Android/data/[app_package_name]/cache/uil-images") if card is mounted and app has + * appropriate permission. Else - Android defines cache directory on device's file system. + * + * @param context Application context + * @return Cache {@link File directory} + */ + public static File getIndividualCacheDirectory(Context context, String uniqueName) { + File cacheDir = getCacheDirectory(context, uniqueName); + File individualCacheDir = new File(cacheDir, INDIVIDUAL_DIR_NAME); + if (!individualCacheDir.exists()) { + if (!individualCacheDir.mkdir()) { + individualCacheDir = cacheDir; + } + } + return individualCacheDir; + } + + /** + * Returns specified application cache directory. Cache directory will be created on SD card by defined path if card + * is mounted and app has appropriate permission. Else - Android defines cache directory on device's file system. + * + * @param context Application context + * @param cacheDir Cache directory path (e.g.: "AppCacheDir", "AppDir/cache/images") + * @return Cache {@link File directory} + */ + public static File getOwnCacheDirectory(Context context, String cacheDir) { + File appCacheDir = null; + if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && hasExternalStoragePermission(context)) { + appCacheDir = new File(FileHelper.getRootCacheDir().getAbsolutePath(), cacheDir); + } + if (appCacheDir == null || (!appCacheDir.exists() && !appCacheDir.mkdirs())) { + appCacheDir = context.getCacheDir(); + } + return appCacheDir; + } + + private static File getExternalCacheDir(Context context) { + File dataDir = new File(new File(FileHelper.getRootCacheDir().getAbsolutePath(), "Android"), "data"); + File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache"); + if (!appCacheDir.exists()) { + if (!appCacheDir.mkdirs()) { + //L.w("Unable to create external cache directory"); + return null; + } + try { + new File(appCacheDir, ".nomedia").createNewFile(); + } catch (IOException e) { + //L.i("Can't create \".nomedia\" file in application external cache directory"); + } + } + return appCacheDir; + } + + public static boolean hasExternalStoragePermission(Context context) { + int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION); + return perm == PackageManager.PERMISSION_GRANTED; + } + + + /** + * 获取多余的挂载点 + * + * @param filterSubdir 是否过滤子文件夹,在设置下载路径的时候需要显示子文件夹 + * @return ArrayList.get(0)是多余的挂载点,如/mnt/sdcard/sd-ext/ + * get(1)是有效的存储目录,如/mnt/sdcard/ + */ + public static ArrayList> getRepeatMountsAndStorage(boolean filterSubdir) { + String storagePath = FileHelper.getRootCacheDir().getAbsolutePath(); + ArrayList> result = new ArrayList>(2); + HashSet repeatMounts = new HashSet(); + HashSet totalStorages = new HashSet(); + HashSet effectiveStorages = new HashSet(); + ArrayList storageInfos = getAllSdcards(storagePath); + if (filterSubdir) { + for (StorageInfo info : storageInfos) { + String path = info.path; + if (!path.endsWith("/")) { + path += "/"; + } + totalStorages.add(path); + if (info.type == StorageInfo.TYPE.Available) { + effectiveStorages.add(path); + } else if (info.type == StorageInfo.TYPE.RepeatMount) { + repeatMounts.add(path); + } + } + + } else { + Collections.sort(storageInfos, new Comparator() { + @Override + public int compare(StorageInfo lhs, StorageInfo rhs) { + return lhs.path.compareTo(rhs.path); + } + }); + StorageInfo info = null, preInfo = null; + for (int i = 0, length = storageInfos.size(); i < length; i++) { + preInfo = info; + info = storageInfos.get(i); + String path = info.path; + if (!path.endsWith("/")) { + info.path += "/"; + } + totalStorages.add(path); + if (info.type == StorageInfo.TYPE.Available + || (info.type == StorageInfo.TYPE.SubStorage && preInfo != null + && info.path.contains(preInfo.path) && preInfo.type == StorageInfo.TYPE.Available)) { + effectiveStorages.add(path); + } else if (info.type == StorageInfo.TYPE.RepeatMount) { + repeatMounts.add(path); + } + } + } + if (effectiveStorages.isEmpty()) { + effectiveStorages + .add(FileHelper.getRootCacheDir().getAbsolutePath() + "/"); + } + repeatMounts.removeAll(effectiveStorages); + result.add(repeatMounts); + result.add(effectiveStorages); + result.add(totalStorages); + return result; + } + + /** + * 最新的获取外置SDCard的方法之前的方法以后都不可用 + * + * @return + */ + private static ArrayList getAllSdcards(String storagePath) { + String mountInfo = fetchMountsInfo(); + ArrayList storageList = paraserSdcards(mountInfo); + File file = new File(storagePath); + if (!isPathContain(storageList, storagePath) && file.exists()) { + StorageInfo info = new StorageInfo(); + info.device = "sdcard"; + info.path = storagePath + "/"; + info.size = file.getTotalSpace(); + storageList.add(info); + } +// PlayMgrLog.log(LogTag.mounts, ResUtil.getString(R.string.utils_file_storageutils_01) + storageList + "\n\r"); + storageList = filterSubSdcards(storageList, storagePath); +// PlayMgrLog.log(LogTag.mounts, ResUtil.getString(R.string.utils_file_storageutils_02) + storageList + "\n\r"); + storageList = filterSameSdcards(storageList, storagePath); +// PlayMgrLog.log(LogTag.mounts, ResUtil.getString(R.string.utils_file_storageutils_03) + storageList + "\n\r"); + return storageList; + } + + private static class StorageInfo { + enum TYPE { + Available, SubStorage, RepeatMount + } + + public StorageInfo() { + + } + + private String path; + + private String device; + + public long size = 0; + + public TYPE type = TYPE.Available; + + @Override + public String toString() { + return String.format(ResUtil.getString(R.string.utils_file_storageutils_04), device, path, size, + type); + } + } + + /** + * 获取手机挂载点信息 + * + * @return + */ + private static String fetchMountsInfo() { + String result = null; + CMDExecute cmdexe = new CMDExecute(); + try { + String[] args = { + "/system/bin/cat", "/proc/mounts" + }; + result = cmdexe.run(args, "/system/bin/"); + } catch (IOException ex) { + ex.printStackTrace(); + } + return result; + } + + /** + * 解析挂在点信息,解析到的为全部的挂在点,可能有重复的 + * + * @param mountInfo + * @return + */ + private static ArrayList paraserSdcards(String mountInfo) { + String reg = "(?i).*(media|vold|fuse).*(vfat|ntfs|exfat|fat32|ext3|ext4|fuse|sdcardfs).*rw.*"; + ArrayList storageInfo = new ArrayList(); + if (StringUtils.isEmpty(mountInfo)) { + return storageInfo; + } + final String[] lines = mountInfo.split("\n"); + for (String line : lines) { + String lowercaseline = line.toLowerCase(); + if (line.contains("secure")) { + continue; + } + if (line.contains("asec")) { + continue; + } + // htc的blinkFeed路径 + if (line.contains("/blinkfeed")) { + continue; + } + // 某款三星的机器,外置SD卡的信息为/mnt/media_rw/extSdCard /storage/extSdCard + // if (line.contains("media")) + // continue; + if (line.contains("system") || line.contains("cache") || line.contains("sys") + || line.contains("data") || line.contains("tmpfs") || line.contains("shell") + || line.contains("root") || line.contains("acct") || line.contains("proc") + || line.contains("misc") || line.contains("obb")) { + continue; + } + if (!lowercaseline.startsWith("/")) { + continue; + } + if (!lowercaseline.matches(reg)) { + continue; + } + // 根据空格分割一条记录为几个字符串 + String[] parts = line.split(" "); + String device = null; + String path = null; + for (String part : parts) { + if (part.startsWith("/")) { + if (part.toLowerCase().contains("vold") || part.toLowerCase().contains("fuse") + || part.toLowerCase().contains("media") + || part.toLowerCase().contains("/data/share")) { + device = part; + } else { + path = part; + } + } + } + if (device != null && path != null && !path.contains("shell")) { + File file = new File(path); + if (!path.endsWith("/")) { + path = path + "/"; + } + // 路径对应的文件必须要存在,排除掉mounts表里面不存在的文件 + if (file.exists() && file.canRead()) { + StorageInfo info = new StorageInfo(); + info.device = device; + info.path = path; + info.size = file.getTotalSpace(); + storageInfo.add(info); + } + } + } + return storageInfo; + } + + private static boolean isPathContain(ArrayList storageList, + final String storagePath) { + for (int i = 0; i < storageList.size(); i++) { + if (storageList.get(i).path.equals(storagePath + "/")) { + return true; + } + } + return false; + } + + /** + * 过滤掉挂在点中的自目录,某些手机中某张SD卡是别的Sd卡的子目录 + * + * @param storageList + * @param storagePath + * @return + */ + private static ArrayList filterSubSdcards(ArrayList storageList, + String storagePath) { + // 过滤SD卡重名路径 + Collections.sort(storageList, new Comparator() { + @Override + public int compare(StorageInfo lhs, StorageInfo rhs) { + return lhs.path.compareTo(rhs.path); + } + }); + for (int i = 0, length = storageList.size(); i < length - 1; i++) { + if (storageList.get(i + 1).path.contains(storageList.get(i).path)) { + storageList.get(i + 1).type = StorageInfo.TYPE.SubStorage; + } + } + return storageList; + + } + + /** + * 过滤相同的文件夹,如果两个文件夹的大小相同并且文件夹中的目录结构有90%是相同的那么就认为这两个文件夹是同一个文件夹 + * + * @param storageList + * @param storagePath 优先保留的文件夹,这部分文件夹路径的话 + * @return + */ + private static ArrayList filterSameSdcards(ArrayList storageList, + final String storagePath) { + // 按照文件夹的大小进行排序 + Collections.sort(storageList, new Comparator() { + @Override + public int compare(StorageInfo lhs, StorageInfo rhs) { + if (lhs.path.equals(rhs.path)) { + return lhs.type.ordinal() - rhs.type.ordinal(); + } + int result = (int) (lhs.size - rhs.size); + if (result != 0) { + return result; + } + if (lhs.path.equals(storagePath + "/")) { + return -1; + } else if (rhs.path.equals(storagePath + "/")) { + return 1; + } else { + return lhs.type.ordinal() - rhs.type.ordinal(); + } + } + }); + StorageInfo info1, info2; + for (int i = 0, length = storageList.size(); i < length - 1; i++) { + info1 = storageList.get(i); + info2 = storageList.get(i + 1); + if (info1.size == info2.size) { + for (int j = i; j < length - 1; j++) { + info2 = storageList.get(j + 1); + if (info1.size != info2.size) { + break; + } + if (info2.type != StorageInfo.TYPE.Available) { + continue; + } + if (isSameDir(info1, info2) || isSameDir(info1, info2) + || isSameDir(info1, info2)) {// 如果不是重复挂在点,判断多次,防止因扫描过程中Sd卡中的文件被改变 + info2.type = StorageInfo.TYPE.RepeatMount; + } + } + } + } + return storageList; + } + + /** + * 判断两个路径是否为同一个路径,如果两个路径的文件结构相同的话,就认为两个文件相同 + * + * @param + * @param + * @return + */ + private static boolean isSameDir(StorageInfo info1, StorageInfo info2) { + if (info1.path.equals(info2.path)) { + return true; + } + File file = new File(info2.path); + if (!file.exists() || !file.isDirectory()) { + return true; + } + File[] files = file.listFiles(); + if (files == null || files.length == 0) { + return true; + } + int sameFileCounts = 0; + File subFile, compareFile; + for (int i = 0; i < files.length; i++) { + subFile = files[i]; + compareFile = new File(info1.path, subFile.getName()); + if (compareFile.exists() && subFile.exists()) { + if (subFile.lastModified() == compareFile.lastModified()) { + sameFileCounts++; + } + } else if (!compareFile.exists() && !subFile.exists()) { + sameFileCounts++; + } + if (i > 10) { + if (((float) sameFileCounts) / i > 0.99) { + return true; + } else if (((float) sameFileCounts) / i < 0.01) { + return false; + } + } + } + if (((float) sameFileCounts) / files.length > 0.9) { + return true; + } else { + return false; + } + } + + public static ArrayList findAllSdCardPaths(boolean filterSubDir) { + HashSet sdCardPath = StorageUtils.getRepeatMountsAndStorage(filterSubDir).get(1); + ArrayList list = new ArrayList<>(); + Iterator it = sdCardPath.iterator(); + while (it.hasNext()) { + String path = it.next(); + if (File.separatorChar == path.charAt(path.length() - 1)) { + path = path.substring(0, path.length() - 1); + } + list.add(path); + } + final String storagePath = FileHelper.getRootCacheDir().getAbsolutePath(); + Collections.sort(list, new Comparator() { + @Override + public int compare(String lhs, String rhs) { + if (lhs.equals(storagePath)) { + return -1; + } else if (rhs.equals(storagePath)) { + return 1; + } else { + return lhs.compareTo(rhs); + } + } + }); + return list; + } + + public static boolean isExternalStorageRemovable() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return Environment.isExternalStorageRemovable(); + } + return true; + } + + public static boolean isExternalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable(); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/file/TargetNotPreparedException.java b/library/src/main/java/com/chwl/library/utils/file/TargetNotPreparedException.java new file mode 100644 index 0000000..472d368 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/file/TargetNotPreparedException.java @@ -0,0 +1,11 @@ +package com.chwl.library.utils.file; + +/** + * Creator: 舒强睿 + * Date:2015/6/24 + * Time:18:40 + *

+ * Description:存储设备不可用异常(sd卡无法识别、sd卡找不到..) + */ +public class TargetNotPreparedException extends CustomFileException { +} diff --git a/library/src/main/java/com/chwl/library/utils/image/Blur.java b/library/src/main/java/com/chwl/library/utils/image/Blur.java new file mode 100644 index 0000000..1d22283 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/image/Blur.java @@ -0,0 +1,287 @@ +package com.chwl.library.utils.image; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import com.chwl.library.utils.log.MLog; + +public class Blur { + + private static final String TAG = "Blur"; + + public static Bitmap fastblur(Context context, Bitmap sentBitmap, int radius) { + return fastblur(context, sentBitmap, radius, false); + } + + @SuppressLint("NewApi") + public static Bitmap fastblur(Context context, Bitmap sentBitmap, int radius, boolean canReuseInBitmap) { + MLog.debug(TAG, "fastblur"); + + if (null == sentBitmap) { + return null; + } + +// if (Build.VERSION.SDK_INT > 16) { +// +// radius = Math.min(radius, 25); +// Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); +// +// final RenderScript rs = RenderScript.create(context.getApplicationContext()); +// final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); +// +// final Allocation input = Allocation.createFromBitmap(rs, sentBitmap, Allocation.MipmapControl.MIPMAP_NONE, +// Allocation.USAGE_SCRIPT); +// final Allocation output = Allocation.createTyped(rs, input.getType()); +// +// script.setRadius(radius /* e.g. 3.f */); +// script.setInput(input); +// script.forEach(output); +// output.copyTo(bitmap); +// +// sentBitmap.recycle(); +// rs.destroy(); +// +// return bitmap; +// } + + // Stack Blur v1.0 from + // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html + // + // Java Author: Mario Klingemann + // http://incubator.quasimondo.com + // created Feburary 29, 2004 + // Android port : Yahel Bouaziz + // http://www.kayenko.com + // ported april 5th, 2012 + + // This is a compromise between Gaussian Blur and Box blur + // It creates much better looking blurs than Box Blur, but is + // 7x faster than my Gaussian Blur implementation. + // + // I called it Stack Blur because this describes best how this + // filter works internally: it creates a kind of moving stack + // of colors whilst scanning through the image. Thereby it + // just has to add one new block of color to the right side + // of the stack and remove the leftmost color. The remaining + // colors on the topmost layer of the stack are either added on + // or reduced by one, depending on if they are on the right or + // on the left side of the stack. + // + // If you are using this algorithm in your code please add + // the following line: + // + // Stack Blur Algorithm by Mario Klingemann + + Bitmap bitmap; + if (canReuseInBitmap) { + bitmap = sentBitmap; + } else { + Bitmap.Config config = sentBitmap.getConfig(); + if (null == config) { + config = Bitmap.Config.ARGB_8888; + } + bitmap = sentBitmap.copy(config, true); + } + + if (radius < 1) { + return (null); + } + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + int[] pix = new int[w * h]; + Log.e("pix", w + " " + h + " " + pix.length); + bitmap.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; + } + } + + Log.e("pix", w + " " + h + " " + pix.length); + bitmap.setPixels(pix, 0, w, 0, 0, w, h); + return (bitmap); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/image/JXImageUtils.java b/library/src/main/java/com/chwl/library/utils/image/JXImageUtils.java new file mode 100644 index 0000000..3c3f770 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/image/JXImageUtils.java @@ -0,0 +1,850 @@ +package com.chwl.library.utils.image; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.utils.LogUtil; +import com.chwl.library.utils.ResUtil; +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.file.JXFileUtils; +import com.chwl.library.utils.log.MLog; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class JXImageUtils { + public static final int IMAGE_COMPRESS_RATE = 80; + public static final int IMAGE_SCALE_WIDTH = 800; + public static final int IMAGE_SCALE_HEIGHT = 800; + public static Bitmap decodeFile(String filePath) { + return decodeFile(filePath, null); + } + + public static Bitmap decodeByWidth(String filePath, int desiredWidth) { + try { + return decodeFileOrThrow(filePath, desiredWidth, 0); + } + catch (Throwable e) { + return null; + } + } + + public static Bitmap decodeBySize(String filePath, int size) { + Rect rect = decodeBmpSize(filePath); + if (rect.width() > rect.height()) { + return decodeByWidth(filePath, size); + } + else { + return decodeByHeight(filePath, size); + } + } + + public static Rect decodeBmpSize(String filePath) { + Options opts = new Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, opts); + return new Rect(0, 0, opts.outWidth, opts.outHeight); + } + + public static Rect decodeBmpSizeBy(String filePath, int desiredWidth, int desiredHeight) { + Options opts = getProperOptions(filePath, desiredWidth, desiredHeight, false); + if (opts == null) { + return null; + } + return new Rect(0, 0, opts.outWidth, opts.outHeight); + } + + public static Bitmap decodeByHeight(String filePath, int desiredHeight) { + try { + return decodeFileOrThrow(filePath, 0, desiredHeight); + } + catch (Throwable e) { + return null; + } + } + + public static Bitmap decodeByWidthOrThrow(String filePath, int desiredWidth) { + return decodeFileOrThrow(filePath, desiredWidth, 0); + } + + public static Bitmap decodeByHeightOrThrow(String filePath, int desiredHeight) { + return decodeFileOrThrow(filePath, 0, desiredHeight); + } + + /** + * Decode file with given options. + * Will prefer use a smaller sample size to save memory, + * If this is not up to demand, use the one with more parameter: + * {@link #decodeFileOrThrow(String, int, int, boolean)}. + * NOTE OutOfMemoryError will be eaten here, and null returned in this case. + * @param filePath File path. + * @param desiredWidth Desired width, can be 0. + * If set to 0, desiredHeight will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param desiredHeight Desired height, can be 0. + * If set to 0, desiredWidth will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @return Bitmap decoded, or null if failed. + */ + public static Bitmap decodeFile(String filePath, int desiredWidth, int desiredHeight) { + try { + return decodeFileOrThrow(filePath, desiredWidth, desiredHeight); + } + catch (Throwable e) { + MLog.warn("decoeFile", "fail to decode %s, %s", filePath, e.toString()); + return null; + } + } + + /** + * Decode file with given options. + * Will prefer use a smaller sample size to save memory, + * If this is not up to demand, use the one with more parameter: + * {@link #decodeFileOrThrow(String, int, int, boolean)}. + * NOTE OutOfMemoryError will be eaten here, and null returned in this case. + * @param resId resId + * @param desiredWidth Desired width, can be 0. + * If set to 0, desiredHeight will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param desiredHeight Desired height, can be 0. + * If set to 0, desiredWidth will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @return Bitmap decoded, or null if failed. + */ + public static Bitmap decodeResource(Context context, int resId, int desiredWidth, int desiredHeight) { + if (desiredWidth <= 0 && desiredHeight <= 0) { + return decodeResource(context, resId); + } + try { + return decodeResOrThrow(context, resId, desiredWidth, desiredHeight, true); + } + catch (Throwable e) { + MLog.error("JXImageUtils", e); + return null; + } + } + + public static Bitmap decodeResource(Context context, int resId) { + try { + final Bitmap res = BitmapFactory.decodeResource(context.getResources(), resId); + return res; + } + catch (OutOfMemoryError e) { + MLog.error("JXImageUtils", e); + } + return null; + } + + /** + * Decode file with given options. + * Will prefer use a smaller sample size to save memory, + * If this is not up to demand, use the one with more parameter: + * {@link #decodeFileOrThrow(String, int, int, boolean)}. + * NOTE OutOfMemoryError can be throw here. + * @param filePath File path. + * @param desiredWidth Desired width, can be 0. + * If set to 0, desiredHeight will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param desiredHeight Desired height, can be 0. + * If set to 0, desiredWidth will be honored. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @return Bitmap decoded, or null if failed. + */ + public static Bitmap decodeFileOrThrow(String filePath, int desiredWidth, int desiredHeight) { + return decodeFileOrThrow(filePath, desiredWidth, desiredHeight, true); + } + + /** + * Decode file with given options. + * NOTE OutOfMemoryError can be throw here. + * @param filePath File path. + * @param desiredWidth Desired width, can be 0. + * If set to 0, maximum width will be used, + * i.e. : desiredHeight will take effect. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param desiredHeight Desired height, can be 0. + * If set to 0, maximum height will be used. + * i.e. : desiredWidth will take effect. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param isMemoryPrior If true, will prefer to use a bigger sample size + * to use less memory, otherwise prefer to use a smaller + * sample size, the the returned bitmap can be with bigger size, + * and can be probably more vivid. + * @return Bitmap decoded, or null if failed. + */ + public static Bitmap decodeFileOrThrow(String filePath, + int desiredWidth, int desiredHeight, boolean isMemoryPrior) { + Options opts = getProperOptions(filePath, desiredWidth, desiredHeight, isMemoryPrior); + if (opts == null) { + return null; + } + opts.inJustDecodeBounds = false; + final Bitmap bmp = BitmapFactory.decodeFile(filePath, opts); + return bmp; + } + + private static Options getProperOptions(String filePath, int desiredWidth, int desiredHeight, + boolean isMemoryPrior) { + Options opts = new Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, opts); + if (opts.outWidth <= 0 || opts.outHeight <= 0) { + return null; + } + + int sampleSize = calSampleSize(desiredWidth, desiredHeight, isMemoryPrior, opts); + + if (desiredHeight > 0 || desiredWidth > 0) { + do { + opts.inSampleSize = sampleSize; + BitmapFactory.decodeFile(filePath, opts); + sampleSize++; + } + while ((desiredWidth > 0 && opts.outWidth > desiredWidth) + || (desiredHeight > 0 && opts.outHeight > desiredHeight)); + } + return opts; + } + + /** + * Decode file with given options. + * NOTE OutOfMemoryError can be throw here. + * @param desiredWidth Desired width, can be 0. + * If set to 0, maximum width will be used, + * i.e. : desiredHeight will take effect. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param desiredHeight Desired height, can be 0. + * If set to 0, maximum height will be used. + * i.e. : desiredWidth will take effect. + * If both desiredWidth and desiredHeight are 0, + * the original bitmap will be decoded. + * @param isMemoryPrior If true, will prefer to use a bigger sample size + * to use less memory, otherwise prefer to use a smaller + * sample size, the the returned bitmap can be with bigger size, + * and can be probably more vivid. + * @return Bitmap decoded, or null if failed. + */ + public static Bitmap decodeResOrThrow(Context context, int drawableId, + int desiredWidth, int desiredHeight, boolean isMemoryPrior) { + Options opts = new Options(); + opts.inJustDecodeBounds = true; + + final Resources res = context.getResources(); + BitmapFactory.decodeResource(res, drawableId, opts); + if (opts.outWidth <= 0 || opts.outHeight <= 0) { + return null; + } + + int sampleSize = calSampleSize(desiredWidth, desiredHeight, isMemoryPrior, opts); + + opts.inJustDecodeBounds = false; + opts.inSampleSize = sampleSize; + final Bitmap ret = BitmapFactory.decodeResource(res, drawableId, opts); + return ret; + } + + private static int calSampleSize(int desiredWidth, int desiredHeight, boolean isMemoryPrior, Options opts) { + int sampleSize = 1; + if (desiredWidth == 0 && desiredHeight == 0) { + sampleSize = 1; + } + else if (desiredHeight == 0) { + sampleSize = (opts.outWidth + desiredWidth - 1) / desiredWidth; + } + else if (desiredWidth == 0) { + sampleSize = (opts.outHeight + desiredHeight - 1) / desiredHeight; + } + else { + final int horRatio = (opts.outWidth + desiredWidth - 1) / desiredWidth; + final int verRatio = (opts.outHeight + desiredHeight - 1) / desiredHeight; + sampleSize = isMemoryPrior ? Math.max(horRatio, verRatio) : Math.min(horRatio, verRatio); + } + return sampleSize; + } + + public static Bitmap decodeFile(String filePath, Options opt) { + if (StringUtils.isEmpty(filePath)) { + return null; + } + Bitmap bmp = null; + try { + File file = new File(filePath); + if (file.isFile()) { + bmp = BitmapFactory.decodeFile(filePath, opt); + } + else { + MLog.error(JXImageUtils.class, filePath + " is not a file"); + } + } catch (OutOfMemoryError err) { + MLog.error(JXImageUtils.class, "oom: " + filePath); + bmp = null; + } + return bmp; + } + + public static Bitmap resizeBitmap(Bitmap bitmap, int maxBorderLength, boolean recycle) { + if (bitmap == null) { + return null; + } + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int newHeight = 0; + int newWidth = 0; + if (width > height) { + float ratio = ((float) height) / ((float) width); + newWidth = maxBorderLength; + newHeight = (int) ((newWidth) * ratio); + } + else if (height > width){ + float ratio = ((float) width) / ((float) height); + newHeight = maxBorderLength; + newWidth = (int) ((newHeight) * ratio); + } + else { + newWidth = maxBorderLength; + newHeight = maxBorderLength; + } + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + Matrix matrix = new Matrix(); + matrix.postScale(scaleWidth, scaleHeight); + try { + Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, + matrix, true); + if (recycle && !bitmap.isRecycled() && bitmap != resizedBitmap) { + bitmap.recycle(); + } + return resizedBitmap; + } + catch (OutOfMemoryError e) { + MLog.error(JXImageUtils.class, "lcy resizeBitmap OOM %s", e); + } + return null; + } + + // resize and rotate image, if matrix is null, then no rotate will be done + public static boolean resizeAndRotateImage(String imageFile, String newFileName, int maxWidth, int maxHeight, Matrix matrix) { + Options options = new Options(); + options.outHeight = 0; + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imageFile, options); + if (options.outWidth <= 0 || options.outHeight <= 0) { + MLog.error(JXImageUtils.class, "bitmap width or height is zero"); + return false; + } + options.inJustDecodeBounds = false; + + int max = options.outWidth; + int min = options.outHeight; + if (options.outWidth < options.outHeight) { + max = options.outHeight; + min = options.outWidth; + } + int sampleSize = 1; + int nextMax = max >> 1; + int nextMin = min >> 1; + + // width is supposed to be bigger in general, but if it is not, just reverse them + if (maxWidth < maxHeight) { + int temp = maxWidth; + maxWidth = maxHeight; + maxHeight = temp; + } + while (nextMax >= maxWidth && nextMin >= maxHeight) { + sampleSize <<= 1; + nextMax >>= 1; + nextMin >>= 1; + } + options.inSampleSize = sampleSize; + + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(imageFile, options); + + + if( matrix != null ) { + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + } + catch (OutOfMemoryError e) { + } + if (bitmap != null) { + try { + JXFileUtils out = JXFileUtils.openFile(newFileName); + out.write(bitmap, IMAGE_COMPRESS_RATE); + out.close(); + return true; + } + catch (Exception e) { + MLog.error(JXImageUtils.class, e); + } + } + return false; + } + + public static int getRotate(String filepath) { + try { + ExifInterface exif = new ExifInterface(filepath); + return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + } + catch (Exception e) { + return 0; + } + } + + public static int getAngleFromRotateEnum(int rotate) { + switch (rotate) { + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; + } + } + + + // resize bitmap if it's size exceeded maxWidth or maxHeight. and return the passed in bitmap if +// no need to change size + public static Bitmap rotateAndResizeImage(Bitmap inBitmap, int maxWidth, int maxHeight, int rotate) { + int imgWidth = inBitmap.getWidth(); + int imgHeight = inBitmap.getHeight(); + boolean needResize = imgWidth > maxWidth || imgHeight > maxHeight; + boolean needRotate = getAngleFromRotateEnum(rotate) != 0; + if (needResize || needRotate) { + Matrix matrix = new Matrix(); + if (needResize) { + float scale = Math.min(maxWidth / (float) imgWidth, maxHeight / (float) imgHeight); + matrix.postScale(scale, scale); + } + if (needRotate) { + matrix.postRotate(getAngleFromRotateEnum(rotate)); + } + try { + Bitmap resultBitmap = Bitmap.createBitmap(inBitmap, 0, 0, imgWidth, imgHeight, matrix, true); + return resultBitmap; + } + catch (OutOfMemoryError e) { + } + } + return inBitmap; + } + + public static Bitmap decodeImageFromStream(InputStream queryStream, InputStream decodeStream) { + Options options = new Options(); + options.outHeight = 0; + options.inJustDecodeBounds = true; + + BitmapFactory.decodeStream(queryStream, null, options); + + if (options.outWidth <= 0 || options.outHeight <= 0) { + MLog.error(JXImageUtils.class, "bitmap width or height is zero"); + return null; + } + options.inJustDecodeBounds = false; + int widthScale = options.outWidth / IMAGE_SCALE_WIDTH; + int heightScale = options.outHeight / IMAGE_SCALE_HEIGHT; + options.inSampleSize = widthScale > heightScale ? widthScale : heightScale; + options.inScaled = false; + + try { + final Bitmap ret = BitmapFactory.decodeStream(decodeStream, null, options); + return ret; + } + catch (OutOfMemoryError e) { + MLog.error("JXImageUtils", "decodeImageFromStream error, OOM"); + } + return null; + } + + public static void saveBitmapToFile(Bitmap bitmap, String filename) throws Exception { + if (bitmap != null && filename != null) { + JXFileUtils out = JXFileUtils.openFile(filename); + out.write(bitmap, IMAGE_COMPRESS_RATE); + out.close(); + } + } + + public static boolean isImage(InputStream queryStream) { + Options options = new Options(); + options.outHeight = 0; + options.inJustDecodeBounds = true; + try { + BitmapFactory.decodeStream(queryStream, null, options); + return (options.outWidth > 0 && options.outHeight > 0); + } + catch (Throwable e) { + return false; + } + } + + public static boolean isImage(File file) { + if (file == null) { + return false; + } + return isImage(file.getPath()); + } + + public static boolean isImage(String imageFile) { + + if (StringUtils.isEmpty(imageFile)) { + return false; + } + Options options = new Options(); + options.outHeight = 0; + options.inJustDecodeBounds = true; + try { + BitmapFactory.decodeFile(imageFile, options); + return (options.outWidth > 0 && options.outHeight > 0); + } + catch (Throwable e) { + MLog.verbose("JXImageUtils", "%d isn't image file", imageFile); + return false; + } + } + + 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 = 0xffffffff; + 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; + } + + private static Bitmap sDefaultMalePhoto = null; + private static Bitmap sDefaultMalePhotoOffline = null; + private static Bitmap sDefaultFemalePhotoOffline = null; + private static Bitmap sDefaultMalePhotoBitmap = null; + private static Bitmap sDefaultFemalePhotoBitmap = null; + + public static boolean isNotDefaultPortrait(Bitmap image) { + return (image != sDefaultFemalePhotoBitmap && image != sDefaultMalePhotoBitmap + && image != sDefaultMalePhotoOffline && image != sDefaultFemalePhotoOffline + && image != sDefaultMalePhoto && image != sDefaultMalePhotoOffline); + } + + public static Bitmap getGrayBmp(final Bitmap image) { + if (image != null) { + try { + Bitmap grayscalBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), + Config.RGB_565); + Canvas canvas = new Canvas(grayscalBitmap); + Paint paint = new Paint(); + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(0); + ColorMatrixColorFilter filter = new ColorMatrixColorFilter(matrix); + paint.setColorFilter(filter); + canvas.drawBitmap(image, 0, 0, paint); + + return grayscalBitmap; + } + catch (Exception e) { + MLog.error("Utils.getGrayBmp", e); + } catch (OutOfMemoryError e) { + MLog.error("Utils.getGrayBmp", e); + } + } + return null; + } + + public static Bitmap createClipBitmap(Bitmap bmp, Rect photoRect) { + // the right and bottom must be checked for their values are converted + // from float math to integer value and might be bigger than actual + // bitmap because of round error + Bitmap portrait = null; + try { + if (bmp != null) { + int bmpWidth = bmp.getWidth(); + int bmpHeight = bmp.getHeight(); + if (bmpWidth > 0 && bmpHeight > 0) { + photoRect.right = photoRect.right > bmpWidth ? bmpWidth + : photoRect.right; + photoRect.bottom = photoRect.bottom > bmpHeight ? bmpHeight + : photoRect.bottom; + portrait = Bitmap.createBitmap(bmp, photoRect.left, + photoRect.top, photoRect.width(), photoRect.height()); + + if (bmp != portrait && !bmp.isRecycled()) // old bitmap is useless, free memory + bmp.recycle(); + } + } + } + catch (Throwable e) { + MLog.debug("hjinw", "e = " + e); + } + return portrait; + } + + public static boolean renameFile(String oriPath, String newPath) { + File file = new File(oriPath); + File newFile = new File(newPath); + return file.renameTo(newFile); + } + + public static Bitmap decodeResource(Context context, int resId, Options opt) { + try { + final Bitmap res = BitmapFactory.decodeResource(context.getResources(), + resId, opt); + return res; + } + catch (OutOfMemoryError e) { + MLog.error("lcy", e); + } + return null; + } + + public static interface PORTRAIT_OPS { + public static final int SMALL = 0; + public static final int BIG = 1; + public static final int ORIGINAL = 2; + } + + /** + * Created blended bitmap for given bitmap. + * This aims to be used for the pressed state of an image icon. + * This can cost much time for a big sized given bitmap. + * + * @param src Cannot be null. + * @return Blended bitmap. + * + */ + public static Bitmap createBlended(Bitmap src) { + if (src == null) { + throw new IllegalArgumentException("Given src is null."); + } + final Bitmap target = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Config.ARGB_8888); + Canvas c = new Canvas(target); + c.drawBitmap(src, 0, 0, null); + c.drawColor(0x8F000000 | (Color.GRAY & 0x00111111)); + + return target; + } + + public static Bitmap resize(Bitmap oriBitmap, int targetWidth, int targetHeight) { + if (oriBitmap == null) { + return null; + } + int width = oriBitmap.getWidth(); + int height = oriBitmap.getHeight(); + float scaleWidth = ((float) targetWidth) / width; + float scaleHeight = ((float) targetHeight) / height; + float scale = scaleWidth > scaleHeight ? scaleHeight : scaleWidth; + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + try { + Bitmap resizedBitmap = Bitmap.createBitmap(oriBitmap, 0, 0, width, height, matrix, true); + return resizedBitmap; + } + catch (OutOfMemoryError e) { + MLog.error(JXImageUtils.class, "resizeBitmap OOM %s", e); + } + return null; + } + + private static final int DEFAULT_JPEG_QUALITY = 90; + public static byte[] compressToBytes(Bitmap bitmap) { + return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY); + } + + public static byte[] compressToBytes(Bitmap bitmap, int quality) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); + return baos.toByteArray(); + } + + + public static CompressResult compressImagePxAndQuality(String inPath, File outDir, String outFileName, int expectWidth, long expectSize) { + try { + if (outDir == null) { + return null; + } + if (!outDir.exists()) { + //创建目录 + outDir.mkdirs(); + } + if (!outDir.exists()) { + return null; + } + + //读取原图的旋转角度,并写入到压缩图片中 + ExifInterface exif = new ExifInterface(inPath); + String orientation = exif.getAttribute(ExifInterface.TAG_ORIENTATION); + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_01) + inPath); + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_02) + getPicRotate(inPath)); + + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inPreferredConfig = Config.RGB_565; + bmOptions.inSampleSize = getExpectInSampleSize(inPath, expectWidth); + Bitmap bitmap = BitmapFactory.decodeFile(inPath, bmOptions); + + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_03) + bitmap.getWidth() + "," + bitmap.getHeight()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int options = 100; + int minQuality = 20; + bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//质量压缩方法,把压缩后的数据存放到baos中 (100表示不压缩,0表示压缩到最小) + Log.e("mouse_debug", ResUtil.getString(R.string.utils_image_jximageutils_04) + baos.toByteArray().length); + while (baos.toByteArray().length > expectSize) {//循环判断如果压缩后图片是否大于指定大小,大于继续压缩 + baos.reset();//重置baos即让下一次的写入覆盖之前的内容 + options -= 5;//图片质量每次减少5 + + if (options <= minQuality) options = minQuality;//如果图片质量小于5,为保证压缩后的图片质量,图片最底压缩质量为5 + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_05) + options); + bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//将压缩后的图片保存到baos中 + if (options == minQuality) break;//如果图片的质量已降到最低则,不再进行压缩 + } + if (!bitmap.isRecycled()) { + bitmap.recycle();//回收内存中的图片 + } + + File thumbnailFile = new File(outDir, outFileName); + if (thumbnailFile.exists()) { + thumbnailFile.delete(); + } + thumbnailFile.createNewFile(); + FileOutputStream fos = new FileOutputStream(thumbnailFile);//将压缩后的图片保存的本地上指定路径中 + fos.write(baos.toByteArray()); + fos.flush(); + fos.close(); + if (thumbnailFile.exists()) { + String compressPath = thumbnailFile.getPath(); + exif = new ExifInterface(compressPath); + if (orientation != null) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation); + exif.saveAttributes(); + } + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_06) + getPicRotate(compressPath)); + int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); + int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); + return new CompressResult(compressPath, width, height, "image/jpeg"); + } + } catch (Exception ex) { + Log.e("mouse_debug", ResUtil.getString(R.string.utils_image_jximageutils_07)); + ex.printStackTrace(); + } catch (OutOfMemoryError error) { + LogUtil.print(ResUtil.getString(R.string.utils_image_jximageutils_08)); + } + return null; + } + + public static int getExpectInSampleSize(String path, int expectWidth) { + //先获取图片旋转角度,如果是90或者270°,则以高作为参考值,否则以宽作为参考值 + int inSampleSize = 1; + BitmapFactory.Options options = new Options(); + options.inJustDecodeBounds = true; + int degree = getPicRotate(path); + boolean isHeight = (degree == 90 || degree == 270); + options.inSampleSize = inSampleSize; + BitmapFactory.decodeFile(path, options); + int imageSize = isHeight ? options.outHeight : options.outWidth; + int maxSampleSize = inSampleSize; + while (imageSize > expectWidth) { + maxSampleSize = inSampleSize; + inSampleSize ++; + options.inSampleSize = inSampleSize; + BitmapFactory.decodeFile(path, options); + imageSize = isHeight ? options.outHeight : options.outWidth; + } + LogUtil.print("inSampleSize=" + inSampleSize); + return maxSampleSize; + } + + /** + * 读取图片属性:旋转的角度 + * + * @param path 图片绝对路径 + * @return degree旋转的角度 + */ + public static int getPicRotate(String path) { + int degree = 0; + try { + ExifInterface exifInterface = new ExifInterface(path); + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + degree = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + degree = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + degree = 270; + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + return degree; + } + + public static class CompressResult { + private String path; + private int width; + private int height; + private String format; + + public CompressResult(String path, int width, int height, String format) { + this.path = path; + this.width = width; + this.height = height; + this.format = format; + } + + public String getPath() { + return path; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String getFormat() { + return format; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/json/JsonParser.java b/library/src/main/java/com/chwl/library/utils/json/JsonParser.java new file mode 100644 index 0000000..046e880 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/json/JsonParser.java @@ -0,0 +1,101 @@ +package com.chwl.library.utils.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import com.chwl.library.utils.log.MLog; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Auto parse json string to object + */ +public class JsonParser { + + private static Gson gson = new GsonBuilder().create(); + + private static com.google.gson.JsonParser jsonParser = new com.google.gson.JsonParser(); + + /** + * parse json string to object + * @param json + * @param clz + * @param should implements Serializable, for proguard keep. + * @return + */ + public static T parseJsonObject(String json, Class clz) { + return gson.fromJson(json, clz); + } + + /** + * parse json string to Array + */ + /*public static T[] parseJsonArray(String json, Class clz) { + T[] result = gson.fromJson(json, new TypeToken() { + }.getType()); + return result; + }*/ + + public static JsonObject parseToJsonObject(String jsonString) { + JsonElement element = jsonParser.parse(jsonString); + JsonObject jsonObject = element.getAsJsonObject(); + return jsonObject; + } + + + /** + * parse json string to Map + */ + public static Map parseJsonMap(String json) { + Map result = gson.fromJson(json, + new TypeToken>() { + }.getType()); + return result; + } + + + public static String toJson(Object obj) { + return gson.toJson(obj); + } + + /** + * + * @param json + * @param clz + * @param should implements Serializable, for proguard keep. + * @return + */ + public static List parseJsonList(String json, Class clz) { + com.google.gson.JsonParser parser = new com.google.gson.JsonParser(); + + try { + JsonElement element = parser.parse(json); + JsonArray array = element.getAsJsonArray(); + List data = new ArrayList(); + for (JsonElement jo : array) { + data.add(gson.fromJson(jo, clz)); + } + return data; + } catch (Exception e) { + MLog.error(JsonParser.class, e); + } + + return null; + } + + public static Map toMap(Object obj) { + return toMap(toJson(obj)); + } + + public static Map toMap(String json) { + Map retMap = gson.fromJson(json, new TypeToken>() {}.getType()); + return retMap; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/json/JsonUtils.java b/library/src/main/java/com/chwl/library/utils/json/JsonUtils.java new file mode 100644 index 0000000..0d66c68 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/json/JsonUtils.java @@ -0,0 +1,51 @@ +package com.chwl.library.utils.json; + +import com.google.gson.Gson; + +/** + * Created by lvzebiao on 2020/2/28. + */ +public class JsonUtils { + + private static Gson gson; + + public static Gson getGson() { + if (gson == null) { + gson = new Gson(); + } + return gson; + } + + /** + * 解析json + * + * @return 如果解析出错,则返回空对象 + */ + public static T fromJson(final String json, final Class type) { + try { + if (json == null || type == null) { + return null; + } + return getGson().fromJson(json, type); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String toJson(Object object) { + String result = null; + try { + if (object != null) { + result = getGson().toJson(object); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + if (result == null) { + result = ""; + } + return result; + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/AutoActivityLifecycleCallback.java b/library/src/main/java/com/chwl/library/utils/keyboard/AutoActivityLifecycleCallback.java new file mode 100644 index 0000000..e008797 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/AutoActivityLifecycleCallback.java @@ -0,0 +1,57 @@ +package com.chwl.library.utils.keyboard; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +/** + * Created by Piasy{github.com/Piasy} on 8/18/16. + */ + +public abstract class AutoActivityLifecycleCallback implements Application.ActivityLifecycleCallbacks { + private final Activity mTargetActivity; + + public AutoActivityLifecycleCallback(Activity targetActivity) { + mTargetActivity = targetActivity; + } + + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + + } + + @Override + public void onActivityStarted(Activity activity) { + + } + + @Override + public void onActivityResumed(Activity activity) { + + } + + @Override + public void onActivityPaused(Activity activity) { + + } + + @Override + public void onActivityStopped(Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (activity == mTargetActivity) { + mTargetActivity.getApplication().unregisterActivityLifecycleCallbacks(this); + onTargetActivityDestroyed(); + } + } + + protected abstract void onTargetActivityDestroyed(); +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardUtil.java b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardUtil.java new file mode 100644 index 0000000..fe92ae7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardUtil.java @@ -0,0 +1,90 @@ +package com.chwl.library.utils.keyboard; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +public final class KeyboardUtil { + + /** + * Supresses instantiation + */ + private KeyboardUtil() { + throw new AssertionError(); + } + + public static float convertDpToPx(Context context, float dp) { + Resources res = context.getResources(); + + return dp * (res.getDisplayMetrics().densityDpi / 160f); + } + + /** + * Show keyboard and focus to given EditText + * + * @param context Context + * @param target EditText to focus + */ + public static void showKeyboard(Context context, EditText target) { + if (context == null || target == null) { + return; + } + + InputMethodManager imm = getInputMethodManager(context.getApplicationContext()); + + imm.showSoftInput(target, InputMethodManager.SHOW_IMPLICIT); + } + + /** + * Show keyboard and focus to given EditText. + * Use this method if target EditText is in Dialog. + * + * @param dialog Dialog + * @param target EditText to focus + */ + public static void showKeyboardInDialog(Dialog dialog, EditText target) { + if (dialog == null || target == null) { + return; + } + + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + target.requestFocus(); + } + + /** + * hide keyboard + * + * @param context Context + * @param target View that currently has focus + */ + public static void hideKeyboard(Context context, View target) { + if (context == null || target == null) { + return; + } + + InputMethodManager imm = getInputMethodManager(context.getApplicationContext()); + imm.hideSoftInputFromWindow(target.getWindowToken(), 0); + } + + /** + * hide keyboard + * + * @param activity Activity + */ + public static void hideKeyboard(Activity activity) { + View view = activity.getWindow().getDecorView(); + + if (view != null) { + hideKeyboard(activity, view); + } + } + + private static InputMethodManager getInputMethodManager(Context context) { + return (InputMethodManager) context.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEvent.java b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEvent.java new file mode 100644 index 0000000..9d94cf7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEvent.java @@ -0,0 +1,115 @@ +package com.chwl.library.utils.keyboard; + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +public class KeyboardVisibilityEvent { + + private final static int KEYBOARD_VISIBLE_THRESHOLD_DP = 100; + + /** + * Set keyboard visibility change event listener. + * This automatically remove registered event listener when the Activity is destroyed + * + * @param activity Activity + * @param listener KeyboardVisibilityEventListener + */ + public static void setEventListener(final Activity activity, + final KeyboardVisibilityEventListener listener) { + + final Unregister unregister = registerEventListener(activity, listener); + activity.getApplication() + .registerActivityLifecycleCallbacks(new AutoActivityLifecycleCallback(activity) { + @Override + protected void onTargetActivityDestroyed() { + unregister.unregister(); + } + }); + } + + /** + * Set keyboard visibility change event listener. + * + * @param activity Activity + * @param listener KeyboardVisibilityEventListener + * @return Unregister + */ + public static Unregister registerEventListener(final Activity activity, + final KeyboardVisibilityEventListener listener) { + + if (activity == null) { + throw new NullPointerException("Parameter:activity must not be null"); + } + +// int softInputMethod = activity.getWindow().getAttributes().softInputMode; +// if(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE != softInputMethod && +// WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED != softInputMethod){ +// throw new IllegalArgumentException("Parameter:activity window SoftInputMethod is not ADJUST_RESIZE"); +// } + + if (listener == null) { + throw new NullPointerException("Parameter:listener must not be null"); + } + + final View activityRoot = getActivityRoot(activity); + + final ViewTreeObserver.OnGlobalLayoutListener layoutListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + + private final Rect r = new Rect(); + + private final int visibleThreshold = Math.round( + KeyboardUtil.convertDpToPx(activity, KEYBOARD_VISIBLE_THRESHOLD_DP)); + + private boolean wasOpened = false; + + @Override + public void onGlobalLayout() { + activityRoot.getWindowVisibleDisplayFrame(r); + + int heightDiff = activityRoot.getRootView().getHeight() - r.height(); + + boolean isOpen = heightDiff > visibleThreshold; + + if (isOpen == wasOpened) { + // keyboard state has not changed + return; + } + + wasOpened = isOpen; + + listener.onVisibilityChanged(isOpen); + } + }; + activityRoot.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); + + return new SimpleUnregister(activity, layoutListener); + } + + /** + * Determine if keyboard is visible + * + * @param activity Activity + * @return Whether keyboard is visible or not + */ + public static boolean isKeyboardVisible(Activity activity) { + Rect r = new Rect(); + + View activityRoot = getActivityRoot(activity); + int visibleThreshold = + Math.round(KeyboardUtil.convertDpToPx(activity, KEYBOARD_VISIBLE_THRESHOLD_DP)); + + activityRoot.getWindowVisibleDisplayFrame(r); + + int heightDiff = activityRoot.getRootView().getHeight() - r.height(); + + return heightDiff > visibleThreshold; + } + + static View getActivityRoot(Activity activity) { + return ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEventListener.java b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEventListener.java new file mode 100644 index 0000000..4230894 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/KeyboardVisibilityEventListener.java @@ -0,0 +1,6 @@ +package com.chwl.library.utils.keyboard; + +public interface KeyboardVisibilityEventListener { + + void onVisibilityChanged(boolean isOpen); +} diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/SimpleUnregister.java b/library/src/main/java/com/chwl/library/utils/keyboard/SimpleUnregister.java new file mode 100644 index 0000000..93f50c7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/SimpleUnregister.java @@ -0,0 +1,41 @@ +package com.chwl.library.utils.keyboard; + +import android.app.Activity; +import android.os.Build; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.lang.ref.WeakReference; + +public class SimpleUnregister implements Unregister { + + private WeakReference mActivityWeakReference; + + private WeakReference mOnGlobalLayoutListenerWeakReference; + + public SimpleUnregister(Activity activity, ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener) { + mActivityWeakReference = new WeakReference<>(activity); + mOnGlobalLayoutListenerWeakReference = new WeakReference<>(globalLayoutListener); + } + + @Override + public void unregister() { + Activity activity = mActivityWeakReference.get(); + ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = mOnGlobalLayoutListenerWeakReference.get(); + + if (null != activity && null != globalLayoutListener) { + View activityRoot = KeyboardVisibilityEvent.getActivityRoot(activity); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + activityRoot.getViewTreeObserver() + .removeOnGlobalLayoutListener(globalLayoutListener); + } else { + activityRoot.getViewTreeObserver() + .removeGlobalOnLayoutListener(globalLayoutListener); + } + } + + mActivityWeakReference.clear(); + mOnGlobalLayoutListenerWeakReference.clear(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/keyboard/Unregister.java b/library/src/main/java/com/chwl/library/utils/keyboard/Unregister.java new file mode 100644 index 0000000..fe2ea6d --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/keyboard/Unregister.java @@ -0,0 +1,13 @@ +package com.chwl.library.utils.keyboard; + +import android.view.ViewTreeObserver; + +public interface Unregister { + + /** + * unregisters the {@link ViewTreeObserver.OnGlobalLayoutListener} and there by does provide any more callback to the {@link KeyboardVisibilityEventListener} + */ + void unregister(); + +} + diff --git a/library/src/main/java/com/chwl/library/utils/log/FastDateFormat.java b/library/src/main/java/com/chwl/library/utils/log/FastDateFormat.java new file mode 100644 index 0000000..8b13709 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/log/FastDateFormat.java @@ -0,0 +1,1660 @@ +package com.chwl.library.utils.log; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + *

+ * FastDateFormat is a fast and thread-safe version of + * {@link SimpleDateFormat}. + *

+ *

+ *

+ * This class can be used as a direct replacement to {@code SimpleDateFormat} in + * most formatting situations. This class is especially useful in multi-threaded + * server environments. {@code SimpleDateFormat} is not thread-safe in any JDK + * version, nor will it be as Sun have closed the bug/RFE. + *

+ *

+ *

+ * Only formatting is supported, but all patterns are compatible with + * SimpleDateFormat (except time zones and some year patterns - see below). + *

+ *

+ *

+ * Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent time + * zones in RFC822 format (eg. {@code +0800} or {@code -1100}). This pattern + * letter can be used here (on all JDK versions). + *

+ *

+ *

+ * In addition, the pattern {@code 'ZZ'} has been made to represent ISO8601 full + * format time zones (eg. {@code +08:00} or {@code -11:00}). This introduces a + * minor incompatibility with Java 1.4, but at a gain of useful functionality. + *

+ *

+ *

+ * Javadoc cites for the year pattern: For formatting, if the number of + * pattern letters is 2, the year is truncated to 2 digits; otherwise it is + * interpreted as a number. Starting with Java 1.7 a pattern of 'Y' or 'YYY' + * will be formatted as '2003', while it was '03' in former Java versions. + * FastDateFormat implements the behavior of Java 7. + *

+ * + * @version $Id: FastDateFormat.java 1146138 2011-07-13 17:01:37Z joehni $ + * @since 2.0 + */ +public class FastDateFormat extends Format { + // A lot of the speed in this class comes from caching, but some comes + // from the special int to StringBuffer conversion. + // + // The following produces a padded 2 digit number: + // buffer.append((char)(value / 10 + '0')); + // buffer.append((char)(value % 10 + '0')); + // + // Note that the fastest append to StringBuffer is a single char (used + // here). + // Note that Integer.toString() is not called, the conversion is simply + // taking the value and adding (mathematically) the ASCII value for '0'. + // So, don't change this code! It works and is very fast. + + /** + * FULL locale dependent date or time style. + */ + public static final int FULL = DateFormat.FULL; + /** + * LONG locale dependent date or time style. + */ + public static final int LONG = DateFormat.LONG; + /** + * MEDIUM locale dependent date or time style. + */ + public static final int MEDIUM = DateFormat.MEDIUM; + /** + * SHORT locale dependent date or time style. + */ + public static final int SHORT = DateFormat.SHORT; + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 1L; + + private static final FormatCache CACHE = new FormatCache() { + @Override + protected FastDateFormat createInstance(String pattern, + TimeZone timeZone, Locale locale) { + return new FastDateFormat(pattern, timeZone, locale); + } + }; + + private static ConcurrentMap cTimeZoneDisplayCache = new ConcurrentHashMap( + 7); + + /** + * The pattern. + */ + private final String mPattern; + /** + * The time zone. + */ + private final TimeZone mTimeZone; + /** + * The locale. + */ + private final Locale mLocale; + /** + * The parsed rules. + */ + private transient Rule[] mRules; + /** + * The estimated maximum length. + */ + private transient int mMaxLengthEstimate; + + // ----------------------------------------------------------------------- + + /** + *

+ * Constructs a new FastDateFormat. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale to use + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDateFormat(String pattern, TimeZone timeZone, Locale locale) { + mPattern = pattern; + mTimeZone = timeZone; + mLocale = locale; + + init(); + } + + /** + *

+ * Gets a formatter instance using the default pattern in the default + * locale. + *

+ * + * @return a date/time formatter + */ + public static FastDateFormat getInstance() { + return CACHE.getDateTimeInstance(SHORT, SHORT, null, null); + } + + /** + *

+ * Gets a formatter instance using the specified pattern in the default + * locale. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid + */ + public static FastDateFormat getInstance(String pattern) { + return CACHE.getInstance(pattern, null, null); + } + + /** + *

+ * Gets a formatter instance using the specified pattern and time zone. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted date + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid + */ + public static FastDateFormat getInstance(String pattern, TimeZone timeZone) { + return CACHE.getInstance(pattern, timeZone, null); + } + + /** + *

+ * Gets a formatter instance using the specified pattern and locale. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @param locale optional locale, overrides system locale + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid + */ + public static FastDateFormat getInstance(String pattern, Locale locale) { + return CACHE.getInstance(pattern, null, locale); + } + + // ----------------------------------------------------------------------- + + /** + *

+ * Gets a formatter instance using the specified pattern, time zone and + * locale. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @param timeZone optional time zone, overrides time zone of formatted date + * @param locale optional locale, overrides system locale + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid or {@code null} + */ + public static FastDateFormat getInstance(String pattern, TimeZone timeZone, + Locale locale) { + return CACHE.getInstance(pattern, timeZone, locale); + } + + /** + *

+ * Gets a date formatter instance using the specified style in the default + * time zone and locale. + *

+ * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @return a localized standard date formatter + * @throws IllegalArgumentException if the Locale has no date pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateInstance(int style) { + return CACHE.getDateTimeInstance(style, null, null, null); + } + + /** + *

+ * Gets a date formatter instance using the specified style and locale in + * the default time zone. + *

+ * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param locale optional locale, overrides system locale + * @return a localized standard date formatter + * @throws IllegalArgumentException if the Locale has no date pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateInstance(int style, Locale locale) { + return CACHE.getDateTimeInstance(style, null, null, locale); + } + + /** + *

+ * Gets a date formatter instance using the specified style and time zone in + * the default locale. + *

+ * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date + * @return a localized standard date formatter + * @throws IllegalArgumentException if the Locale has no date pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateInstance(int style, TimeZone timeZone) { + return CACHE.getDateTimeInstance(style, null, timeZone, null); + } + + // ----------------------------------------------------------------------- + + /** + *

+ * Gets a date formatter instance using the specified style, time zone and + * locale. + *

+ * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date + * @param locale optional locale, overrides system locale + * @return a localized standard date formatter + * @throws IllegalArgumentException if the Locale has no date pattern defined + */ + public static FastDateFormat getDateInstance(int style, TimeZone timeZone, + Locale locale) { + return CACHE.getDateTimeInstance(style, null, timeZone, locale); + } + + /** + *

+ * Gets a time formatter instance using the specified style in the default + * time zone and locale. + *

+ * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @return a localized standard time formatter + * @throws IllegalArgumentException if the Locale has no time pattern defined + * @since 2.1 + */ + public static FastDateFormat getTimeInstance(int style) { + return CACHE.getDateTimeInstance(null, style, null, null); + } + + /** + *

+ * Gets a time formatter instance using the specified style and locale in + * the default time zone. + *

+ * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param locale optional locale, overrides system locale + * @return a localized standard time formatter + * @throws IllegalArgumentException if the Locale has no time pattern defined + * @since 2.1 + */ + public static FastDateFormat getTimeInstance(int style, Locale locale) { + return CACHE.getDateTimeInstance(null, style, null, locale); + } + + /** + *

+ * Gets a time formatter instance using the specified style and time zone in + * the default locale. + *

+ * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @return a localized standard time formatter + * @throws IllegalArgumentException if the Locale has no time pattern defined + * @since 2.1 + */ + public static FastDateFormat getTimeInstance(int style, TimeZone timeZone) { + return CACHE.getDateTimeInstance(null, style, timeZone, null); + } + + // ----------------------------------------------------------------------- + + /** + *

+ * Gets a time formatter instance using the specified style, time zone and + * locale. + *

+ * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @param locale optional locale, overrides system locale + * @return a localized standard time formatter + * @throws IllegalArgumentException if the Locale has no time pattern defined + */ + public static FastDateFormat getTimeInstance(int style, TimeZone timeZone, + Locale locale) { + return CACHE.getDateTimeInstance(null, style, timeZone, locale); + } + + /** + *

+ * Gets a date/time formatter instance using the specified style in the + * default time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateTimeInstance(int dateStyle, + int timeStyle) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, null, null); + } + + /** + *

+ * Gets a date/time formatter instance using the specified style and locale + * in the default time zone. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateTimeInstance(int dateStyle, + int timeStyle, Locale locale) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, null, locale); + } + + /** + *

+ * Gets a date/time formatter instance using the specified style and time + * zone in the default locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + * @since 2.1 + */ + public static FastDateFormat getDateTimeInstance(int dateStyle, + int timeStyle, TimeZone timeZone) { + return getDateTimeInstance(dateStyle, timeStyle, timeZone, null); + } + + // ----------------------------------------------------------------------- + + /** + *

+ * Gets a date/time formatter instance using the specified style, time zone + * and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + public static FastDateFormat getDateTimeInstance(int dateStyle, + int timeStyle, TimeZone timeZone, Locale locale) { + return CACHE.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale); + } + + // Constructor + // ----------------------------------------------------------------------- + + /** + *

+ * Gets the time zone display name, using a cache for performance. + *

+ * + * @param tz the zone to query + * @param daylight true if daylight savings + * @param style the style to use {@code TimeZone.LONG} or + * {@code TimeZone.SHORT} + * @param locale the locale to use + * @return the textual name of the time zone + */ + static String getTimeZoneDisplay(TimeZone tz, boolean daylight, int style, + Locale locale) { + TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, + locale); + String value = cTimeZoneDisplayCache.get(key); + if (value == null) { + // This is a very slow call, so cache the results. + value = tz.getDisplayName(daylight, style, locale); + String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); + if (prior != null) { + value = prior; + } + } + return value; + } + + /** + *

+ * Initializes the instance for first use. + *

+ */ + private void init() { + List rulesList = parsePattern(); + mRules = rulesList.toArray(new Rule[rulesList.size()]); + + int len = 0; + for (int i = mRules.length; --i >= 0; ) { + len += mRules[i].estimateLength(); + } + + mMaxLengthEstimate = len; + } + + // Parse the pattern + // ----------------------------------------------------------------------- + + /** + *

+ * Returns a list of Rules given a pattern. + *

+ * + * @return a {@code List} of Rule objects + * @throws IllegalArgumentException if pattern is invalid + */ + protected List parsePattern() { + DateFormatSymbols symbols = new DateFormatSymbols(mLocale); + List rules = new ArrayList(); + + String[] eras = symbols.getEras(); + String[] months = symbols.getMonths(); + String[] shortMonths = symbols.getShortMonths(); + String[] weekdays = symbols.getWeekdays(); + String[] shortWeekdays = symbols.getShortWeekdays(); + String[] amPmStrings = symbols.getAmPmStrings(); + + int length = mPattern.length(); + int[] indexRef = new int[1]; + + for (int i = 0; i < length; i++) { + indexRef[0] = i; + String token = parseToken(mPattern, indexRef); + i = indexRef[0]; + + int tokenLen = token.length(); + if (tokenLen == 0) { + break; + } + + Rule rule; + char c = token.charAt(0); + + switch (c) { + case 'G': // era designator (text) + rule = new TextField(Calendar.ERA, eras); + break; + case 'y': // year (number) + if (tokenLen == 2) { + rule = TwoDigitYearField.INSTANCE; + } else { + rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 + : tokenLen); + } + break; + case 'M': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, months); + } else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, shortMonths); + } else if (tokenLen == 2) { + rule = TwoDigitMonthField.INSTANCE; + } else { + rule = UnpaddedMonthField.INSTANCE; + } + break; + case 'd': // day in month (number) + rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); + break; + case 'h': // hour in am/pm (number, 1..12) + rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, + tokenLen)); + break; + case 'H': // hour in day (number, 0..23) + rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); + break; + case 'm': // minute in hour (number) + rule = selectNumberRule(Calendar.MINUTE, tokenLen); + break; + case 's': // second in minute (number) + rule = selectNumberRule(Calendar.SECOND, tokenLen); + break; + case 'S': // millisecond (number) + rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); + break; + case 'E': // day in week (text) + rule = new TextField(Calendar.DAY_OF_WEEK, + tokenLen < 4 ? shortWeekdays : weekdays); + break; + case 'D': // day in year (number) + rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); + break; + case 'F': // day of week in month (number) + rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); + break; + case 'w': // week in year (number) + rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); + break; + case 'W': // week in month (number) + rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); + break; + case 'a': // am/pm marker (text) + rule = new TextField(Calendar.AM_PM, amPmStrings); + break; + case 'k': // hour in day (1..24) + rule = new TwentyFourHourField(selectNumberRule( + Calendar.HOUR_OF_DAY, tokenLen)); + break; + case 'K': // hour in am/pm (0..11) + rule = selectNumberRule(Calendar.HOUR, tokenLen); + break; + case 'z': // time zone (text) + if (tokenLen >= 4) { + rule = new TimeZoneNameRule(mTimeZone, mLocale, + TimeZone.LONG); + } else { + rule = new TimeZoneNameRule(mTimeZone, mLocale, + TimeZone.SHORT); + } + break; + case 'Z': // time zone (value) + if (tokenLen == 1) { + rule = TimeZoneNumberRule.INSTANCE_NO_COLON; + } else { + rule = TimeZoneNumberRule.INSTANCE_COLON; + } + break; + case '\'': // literal text + String sub = token.substring(1); + if (sub.length() == 1) { + rule = new CharacterLiteral(sub.charAt(0)); + } else { + rule = new StringLiteral(sub); + } + break; + default: + throw new IllegalArgumentException( + "Illegal pattern component: " + token); + } + + rules.add(rule); + } + + return rules; + } + + /** + *

+ * Performs the parsing of tokens. + *

+ * + * @param pattern the pattern + * @param indexRef index references + * @return parsed token + */ + protected String parseToken(String pattern, int[] indexRef) { + StringBuilder buf = new StringBuilder(); + + int i = indexRef[0]; + int length = pattern.length(); + + char c = pattern.charAt(i); + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + // Scan a run of the same character, which indicates a time + // pattern. + buf.append(c); + + while (i + 1 < length) { + char peek = pattern.charAt(i + 1); + if (peek == c) { + buf.append(c); + i++; + } else { + break; + } + } + } else { + // This will identify token as text. + buf.append('\''); + + boolean inLiteral = false; + + for (; i < length; i++) { + c = pattern.charAt(i); + + if (c == '\'') { + if (i + 1 < length && pattern.charAt(i + 1) == '\'') { + // '' is treated as escaped ' + i++; + buf.append(c); + } else { + inLiteral = !inLiteral; + } + } else if (!inLiteral + && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { + i--; + break; + } else { + buf.append(c); + } + } + } + + indexRef[0] = i; + return buf.toString(); + } + + /** + *

+ * Gets an appropriate rule for the padding required. + *

+ * + * @param field the field to get a rule for + * @param padding the padding required + * @return a new rule with the correct padding + */ + protected NumberRule selectNumberRule(int field, int padding) { + switch (padding) { + case 1: + return new UnpaddedNumberField(field); + case 2: + return new TwoDigitNumberField(field); + default: + return new PaddedNumberField(field, padding); + } + } + + // Format methods + // ----------------------------------------------------------------------- + + /** + *

+ * Formats a {@code Date}, {@code Calendar} or {@code Long} (milliseconds) + * object. + *

+ * + * @param obj the object to format + * @param toAppendTo the buffer to append to + * @param pos the position - ignored + * @return the buffer passed in + */ + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + if (obj instanceof Date) { + return format((Date) obj, toAppendTo); + } else if (obj instanceof Calendar) { + return format((Calendar) obj, toAppendTo); + } else if (obj instanceof Long) { + return format(((Long) obj).longValue(), toAppendTo); + } else { + throw new IllegalArgumentException("Unknown class: " + + (obj == null ? "" : obj.getClass().getName())); + } + } + + /** + *

+ * Formats a millisecond {@code long} value. + *

+ * + * @param millis the millisecond value to format + * @return the formatted string + * @since 2.1 + */ + public String format(long millis) { + return format(new Date(millis)); + } + + /** + *

+ * Formats a {@code Date} object using a {@code GregorianCalendar}. + *

+ * + * @param date the date to format + * @return the formatted string + */ + public String format(Date date) { + Calendar c = new GregorianCalendar(mTimeZone, mLocale); // hard code + // GregorianCalendar + c.setTime(date); + return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString(); + } + + /** + *

+ * Formats a {@code Calendar} object. + *

+ * + * @param calendar the calendar to format + * @return the formatted string + */ + public String format(Calendar calendar) { + return format(calendar, new StringBuffer(mMaxLengthEstimate)) + .toString(); + } + + /** + *

+ * Formats a milliseond {@code long} value into the supplied + * {@code StringBuffer}. + *

+ * + * @param millis the millisecond value to format + * @param buf the buffer to format into + * @return the specified string buffer + * @since 2.1 + */ + public StringBuffer format(long millis, StringBuffer buf) { + return format(new Date(millis), buf); + } + + /** + *

+ * Formats a {@code Date} object into the supplied {@code StringBuffer} + * using a {@code GregorianCalendar}. + *

+ * + * @param date the date to format + * @param buf the buffer to format into + * @return the specified string buffer + */ + public StringBuffer format(Date date, StringBuffer buf) { + Calendar c = new GregorianCalendar(mTimeZone, mLocale); // hard code + // GregorianCalendar + c.setTime(date); + return applyRules(c, buf); + } + + /** + *

+ * Formats a {@code Calendar} object into the supplied {@code StringBuffer}. + *

+ * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @return the specified string buffer + */ + public StringBuffer format(Calendar calendar, StringBuffer buf) { + return applyRules(calendar, buf); + } + + /** + *

+ * Performs the formatting by applying the rules to the specified calendar. + *

+ * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @return the specified string buffer + */ + protected StringBuffer applyRules(Calendar calendar, StringBuffer buf) { + for (Rule rule : mRules) { + rule.appendTo(buf, calendar); + } + return buf; + } + + // Parsing + // ----------------------------------------------------------------------- + + /** + *

+ * Parsing is not supported. + *

+ * + * @param source the string to parse + * @param pos the parsing position + * @return {@code null} as not supported + */ + @Override + public Object parseObject(String source, ParsePosition pos) { + pos.setIndex(0); + pos.setErrorIndex(0); + return null; + } + + // Accessors + // ----------------------------------------------------------------------- + + /** + *

+ * Gets the pattern used by this formatter. + *

+ * + * @return the pattern, {@link SimpleDateFormat} compatible + */ + public String getPattern() { + return mPattern; + } + + /** + *

+ * Gets the time zone used by this formatter. + *

+ *

+ *

+ * This zone is always used for {@code Date} formatting. + *

+ * + * @return the time zone + */ + public TimeZone getTimeZone() { + return mTimeZone; + } + + /** + *

+ * Gets the locale used by this formatter. + *

+ * + * @return the locale + */ + public Locale getLocale() { + return mLocale; + } + + /** + *

+ * Gets an estimate for the maximum string length that the formatter will + * produce. + *

+ *

+ *

+ * The actual formatted length will almost always be less than or equal to + * this amount. + *

+ * + * @return the maximum formatted length + */ + public int getMaxLengthEstimate() { + return mMaxLengthEstimate; + } + + // Basics + // ----------------------------------------------------------------------- + + /** + *

+ * Compares two objects for equality. + *

+ * + * @param obj the object to compare to + * @return {@code true} if equal + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof FastDateFormat == false) { + return false; + } + FastDateFormat other = (FastDateFormat) obj; + return mPattern.equals(other.mPattern) + && mTimeZone.equals(other.mTimeZone) + && mLocale.equals(other.mLocale); + } + + /** + *

+ * Returns a hashcode compatible with equals. + *

+ * + * @return a hashcode compatible with equals + */ + @Override + public int hashCode() { + return mPattern.hashCode() + 13 + * (mTimeZone.hashCode() + 13 * mLocale.hashCode()); + } + + /** + *

+ * Gets a debugging string version of this formatter. + *

+ * + * @return a debugging string + */ + @Override + public String toString() { + return "FastDateFormat[" + mPattern + "]"; + } + + // Serializing + // ----------------------------------------------------------------------- + + /** + * Create the object after serialization. This implementation reinitializes + * the transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + init(); + } + + // Rules + // ----------------------------------------------------------------------- + + /** + *

+ * Inner class defining a rule. + *

+ */ + private interface Rule { + /** + * Returns the estimated lentgh of the result. + * + * @return the estimated length + */ + int estimateLength(); + + /** + * Appends the value of the specified calendar to the output buffer + * based on the rule implementation. + * + * @param buffer the output buffer + * @param calendar calendar to be appended + */ + void appendTo(StringBuffer buffer, Calendar calendar); + } + + /** + *

+ * Inner class defining a numeric rule. + *

+ */ + private interface NumberRule extends Rule { + /** + * Appends the specified value to the output buffer based on the rule + * implementation. + * + * @param buffer the output buffer + * @param value the value to be appended + */ + void appendTo(StringBuffer buffer, int value); + } + + /** + *

+ * Inner class to output a constant single character. + *

+ */ + private static class CharacterLiteral implements Rule { + private final char mValue; + + /** + * Constructs a new instance of {@code CharacterLiteral} to hold the + * specified value. + * + * @param value the character literal + */ + CharacterLiteral(char value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 1; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output a constant string. + *

+ */ + private static class StringLiteral implements Rule { + private final String mValue; + + /** + * Constructs a new instance of {@code StringLiteral} to hold the + * specified value. + * + * @param value the string literal + */ + StringLiteral(String value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return mValue.length(); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output one of a set of values. + *

+ */ + private static class TextField implements Rule { + private final int mField; + private final String[] mValues; + + /** + * Constructs an instance of {@code TextField} with the specified field + * and values. + * + * @param field the field + * @param values the field values + */ + TextField(int field, String[] values) { + mField = field; + mValues = values; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + int max = 0; + for (int i = mValues.length; --i >= 0; ) { + int len = mValues[i].length(); + if (len > max) { + max = len; + } + } + return max; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + buffer.append(mValues[calendar.get(mField)]); + } + } + + /** + *

+ * Inner class to output an unpadded number. + *

+ */ + private static class UnpaddedNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code UnpadedNumberField} with the + * specified field. + * + * @param field the field + */ + UnpaddedNumberField(int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + if (value < 10) { + buffer.append((char) (value + '0')); + } else if (value < 100) { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } else { + buffer.append(Integer.toString(value)); + } + } + } + + /** + *

+ * Inner class to output an unpadded month. + *

+ */ + private static class UnpaddedMonthField implements NumberRule { + static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); + + /** + * Constructs an instance of {@code UnpaddedMonthField}. + */ + UnpaddedMonthField() { + super(); + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + if (value < 10) { + buffer.append((char) (value + '0')); + } else { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + } + } + + /** + *

+ * Inner class to output a padded number. + *

+ */ + private static class PaddedNumberField implements NumberRule { + private final int mField; + private final int mSize; + + /** + * Constructs an instance of {@code PaddedNumberField}. + * + * @param field the field + * @param size size of the output field + */ + PaddedNumberField(int field, int size) { + if (size < 3) { + // Should use UnpaddedNumberField or TwoDigitNumberField. + throw new IllegalArgumentException(); + } + mField = field; + mSize = size; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + if (value < 100) { + for (int i = mSize; --i >= 2; ) { + buffer.append('0'); + } + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } else { + int digits; + if (value < 1000) { + digits = 3; + } else { + digits = Integer.toString(value).length(); + } + for (int i = mSize; --i >= digits; ) { + buffer.append('0'); + } + buffer.append(Integer.toString(value)); + } + } + } + + /** + *

+ * Inner class to output a two digit number. + *

+ */ + private static class TwoDigitNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code TwoDigitNumberField} with the + * specified field. + * + * @param field the field + */ + TwoDigitNumberField(int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + if (value < 100) { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } else { + buffer.append(Integer.toString(value)); + } + } + } + + /** + *

+ * Inner class to output a two digit year. + *

+ */ + private static class TwoDigitYearField implements NumberRule { + static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); + + /** + * Constructs an instance of {@code TwoDigitYearField}. + */ + TwoDigitYearField() { + super(); + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + } + + /** + *

+ * Inner class to output a two digit month. + *

+ */ + private static class TwoDigitMonthField implements NumberRule { + static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); + + /** + * Constructs an instance of {@code TwoDigitMonthField}. + */ + TwoDigitMonthField() { + super(); + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + public final void appendTo(StringBuffer buffer, int value) { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + } + + /** + *

+ * Inner class to output the twelve hour field. + *

+ */ + private static class TwelveHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwelveHourField} with the specified + * {@code NumberRule}. + * + * @param rule the rule + */ + TwelveHourField(NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + int value = calendar.get(Calendar.HOUR); + if (value == 0) { + value = calendar.getLeastMaximum(Calendar.HOUR) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, int value) { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the twenty four hour field. + *

+ */ + private static class TwentyFourHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwentyFourHourField} with the + * specified {@code NumberRule}. + * + * @param rule the rule + */ + TwentyFourHourField(NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + int value = calendar.get(Calendar.HOUR_OF_DAY); + if (value == 0) { + value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, int value) { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output a time zone name. + *

+ */ + private static class TimeZoneNameRule implements Rule { + private final TimeZone mTimeZone; + private final String mStandard; + private final String mDaylight; + + /** + * Constructs an instance of {@code TimeZoneNameRule} with the specified + * properties. + * + * @param timeZone the time zone + * @param locale the locale + * @param style the style + */ + TimeZoneNameRule(TimeZone timeZone, Locale locale, int style) { + mTimeZone = timeZone; + + mStandard = getTimeZoneDisplay(timeZone, false, style, locale); + mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return Math.max(mStandard.length(), mDaylight.length()); + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + if (mTimeZone.useDaylightTime() + && calendar.get(Calendar.DST_OFFSET) != 0) { + buffer.append(mDaylight); + } else { + buffer.append(mStandard); + } + } + } + + /** + *

+ * Inner class to output a time zone as a number {@code +/-HHMM} or + * {@code +/-HH:MM}. + *

+ */ + private static class TimeZoneNumberRule implements Rule { + static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule( + true); + static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule( + false); + + final boolean mColon; + + /** + * Constructs an instance of {@code TimeZoneNumberRule} with the + * specified properties. + * + * @param colon add colon between HH and MM in the output if {@code true} + */ + TimeZoneNumberRule(boolean colon) { + mColon = colon; + } + + /** + * {@inheritDoc} + */ + public int estimateLength() { + return 5; + } + + /** + * {@inheritDoc} + */ + public void appendTo(StringBuffer buffer, Calendar calendar) { + int offset = calendar.get(Calendar.ZONE_OFFSET) + + calendar.get(Calendar.DST_OFFSET); + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + int hours = offset / (60 * 60 * 1000); + buffer.append((char) (hours / 10 + '0')); + buffer.append((char) (hours % 10 + '0')); + + if (mColon) { + buffer.append(':'); + } + + int minutes = offset / (60 * 1000) - 60 * hours; + buffer.append((char) (minutes / 10 + '0')); + buffer.append((char) (minutes % 10 + '0')); + } + } + + // ---------------------------------------------------------------------- + + /** + *

+ * Inner class that acts as a compound key for time zone names. + *

+ */ + private static class TimeZoneDisplayKey { + private final TimeZone mTimeZone; + private final int mStyle; + private final Locale mLocale; + + /** + * Constructs an instance of {@code TimeZoneDisplayKey} with the + * specified properties. + * + * @param timeZone the time zone + * @param daylight adjust the style for daylight saving time if {@code true} + * @param style the timezone style + * @param locale the timezone locale + */ + TimeZoneDisplayKey(TimeZone timeZone, boolean daylight, int style, + Locale locale) { + mTimeZone = timeZone; + if (daylight) { + style |= 0x80000000; + } + mStyle = style; + mLocale = locale; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (mStyle * 31 + mLocale.hashCode()) * 31 + + mTimeZone.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TimeZoneDisplayKey) { + TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj; + return mTimeZone.equals(other.mTimeZone) + && mStyle == other.mStyle + && mLocale.equals(other.mLocale); + } + return false; + } + } +} diff --git a/library/src/main/java/com/chwl/library/utils/log/FormatCache.java b/library/src/main/java/com/chwl/library/utils/log/FormatCache.java new file mode 100644 index 0000000..77935b1 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/log/FormatCache.java @@ -0,0 +1,223 @@ +package com.chwl.library.utils.log; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + *

+ * FormatCache is a cache and factory for {@link Format}s. + *

+ * + * @version $Id: FormatCache 892161 2009-12-18 07:21:10Z $ + * @since 3.0 + */ +// TODO: Before making public move from getDateTimeInstance(Integer,...) to int; +// or some other approach. +abstract class FormatCache { + /** + * No date or no time. Used in same parameters as DateFormat.SHORT or + * DateFormat.LONG + */ + static final int NONE = -1; + + private final ConcurrentMap cInstanceCache = new ConcurrentHashMap( + 7); + + private final ConcurrentMap cDateTimeInstanceCache = new ConcurrentHashMap( + 7); + + /** + *

+ * Gets a formatter instance using the default pattern in the default + * timezone and locale. + *

+ * + * @return a date/time formatter + */ + public F getInstance() { + return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, + TimeZone.getDefault(), Locale.getDefault()); + } + + /** + *

+ * Gets a formatter instance using the specified pattern, time zone and + * locale. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern + * @param timeZone the non-null time zone + * @param locale the non-null locale + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid or null + */ + public F getInstance(String pattern, TimeZone timeZone, Locale locale) { + if (pattern == null) { + throw new NullPointerException("pattern must not be null"); + } + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + if (locale == null) { + locale = Locale.getDefault(); + } + MultipartKey key = new MultipartKey(pattern, timeZone, locale); + F format = cInstanceCache.get(key); + if (format == null) { + format = createInstance(pattern, timeZone, locale); + F previousValue = cInstanceCache.putIfAbsent(key, format); + if (previousValue != null) { + // another thread snuck in and did the same work + // we should return the instance that is in ConcurrentMap + format = previousValue; + } + } + return format; + } + + /** + *

+ * Create a format instance using the specified pattern, time zone and + * locale. + *

+ * + * @param pattern {@link SimpleDateFormat} compatible pattern, this + * will not be null. + * @param timeZone time zone, this will not be null. + * @param locale locale, this will not be null. + * @return a pattern based date/time formatter + * @throws IllegalArgumentException if pattern is invalid or null + */ + abstract protected F createInstance(String pattern, TimeZone timeZone, + Locale locale); + + /** + *

+ * Gets a date/time formatter instance using the specified style, time zone + * and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + public F getDateTimeInstance(Integer dateStyle, Integer timeStyle, + TimeZone timeZone, Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + MultipartKey key = new MultipartKey(dateStyle, timeStyle, locale); + + String pattern = cDateTimeInstanceCache.get(key); + if (pattern == null) { + try { + DateFormat formatter; + if (dateStyle == null) { + formatter = DateFormat.getTimeInstance(timeStyle, locale); + } else if (timeStyle == null) { + formatter = DateFormat.getDateInstance(dateStyle, locale); + } else { + formatter = DateFormat.getDateTimeInstance(dateStyle, + timeStyle, locale); + } + pattern = ((SimpleDateFormat) formatter).toPattern(); + String previous = cDateTimeInstanceCache.putIfAbsent(key, + pattern); + if (previous != null) { + // even though it doesn't matter if another thread put the + // pattern + // it's still good practice to return the String instance + // that is + // actually in the ConcurrentMap + pattern = previous; + } + } catch (ClassCastException ex) { + throw new IllegalArgumentException( + "No date time pattern for locale: " + locale); + } + } + + return getInstance(pattern, timeZone, locale); + } + + // ---------------------------------------------------------------------- + + /** + *

+ * Helper class to hold multi-part Map keys + *

+ */ + private static class MultipartKey { + private final Object[] keys; + private int hashCode; + + /** + * Constructs an instance of MultipartKey to hold the + * specified objects. + * + * @param keys the set of objects that make up the key. Each key may be + * null. + */ + public MultipartKey(Object... keys) { + this.keys = keys; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof MultipartKey == false) { + return false; + } + return Arrays.equals(keys, ((MultipartKey) obj).keys); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int rc = 0; + for (Object key : keys) { + if (key != null) { + rc = rc * 7 + key.hashCode(); + } + } + hashCode = rc; + } + return hashCode; + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/log/LogToES.java b/library/src/main/java/com/chwl/library/utils/log/LogToES.java new file mode 100644 index 0000000..e63afda --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/log/LogToES.java @@ -0,0 +1,347 @@ +package com.chwl.library.utils.log; + +import android.os.SystemClock; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; + +public class LogToES { + /** + * In MB. + */ + public static final int MAX_FILE_SIZE = 3;//修改日志最大文件为3M,因为反馈系统有超过1M的限制(压缩后1M) + public static final int DEFAULT_BAK_FILE_NUM_LIMIT = 2; + /** + * Back file num limit, when this is exceeded, will delete older logs. + */ + private static int mBackFileNumLimit = DEFAULT_BAK_FILE_NUM_LIMIT; + public static final int DEFAULT_BUFF_SIZE = 32 * 1024; + /** + * Buffer size , threshold for flush/close. + */ + private static int BUFF_SIZE = DEFAULT_BUFF_SIZE; + private static final String BAK_EXT = ".bak"; + private static final FastDateFormat LOG_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd kk:mm:ss"); + /** + * 10 days. + */ + private static final long DAY_DELAY = 10L * 24 * 60 * 60 * 1000; + private static final long FLUSH_INTERVAL = 5000; + private static FastDateFormat simpleDateFormat = FastDateFormat.getInstance("-MM-dd-kk-mm-ss"); + private static Object mLock = new Object(); + /** + * These two are protected by mLock. + */ + private static BufferedWriter mWriter; + private static String mPath; + /** + * To flush by interval. + */ + private static long mLastMillis = 0; + private volatile static String mLogPath; + + public static void setBackupLogLimitInMB(int logCapacityInMB) { + mBackFileNumLimit = (logCapacityInMB + MAX_FILE_SIZE - 1) / MAX_FILE_SIZE; + } + + public static boolean setLogPath(String logDir) { + if (logDir == null || logDir.length() == 0) { + return false; + } + mLogPath = logDir; + + new File(logDir).mkdirs(); + + return new File(logDir).isDirectory(); + } + + public static String getLogPath() { + return mLogPath; + } + + public static void setBuffSize(int bytes) { + BUFF_SIZE = bytes; + } + + public static void writeLogToFile(String dir, String fileName, String msg, + boolean immediateClose, long timeMillis) throws IOException { + writeLog(dir, fileName, msg, immediateClose, timeMillis); + } + + public static void writeLog(String path, String fileName, String msg, + boolean immediateClose, long timeMillis) throws IOException { + if (path == null || path.length() == 0 || fileName == null || fileName.length() == 0) { + return; + } + File dirFile = new File(path); + if (!dirFile.exists()) { + dirFile.mkdirs(); + } + + boolean needCreate = false; + + File logFile = createFile(path, fileName); + if (!logFile.exists()) { + try { + logFile.createNewFile(); + } catch (IOException e) { + MLog.error("LogToES", "writeLog error! " + e.toString()); + return; + } + } else { + long fileSize = (logFile.length() >>> 20);// convert to M bytes + if (fileSize > MAX_FILE_SIZE) { + deleteOldLogs(); + + String fileExt = simpleDateFormat.format(timeMillis); + + StringBuilder sb = new StringBuilder(path); + sb.append(File.separator).append(fileName).append(fileExt) + .append(BAK_EXT); + + close(); + + File fileNameTo = new File(sb.toString()); + + logFile.renameTo(fileNameTo); + logFile = createFile(path, fileName); + needCreate = true; + + limitVolume(); + } + } + + String strLog = LOG_FORMAT.format(timeMillis); + + StringBuffer sb = new StringBuffer(strLog); + sb.append(' '); + sb.append(msg); + sb.append('\n'); + strLog = sb.toString(); + + synchronized (mLock) { + if (mPath == null) { + mPath = logFile.getAbsolutePath(); + needCreate = true; + } else if (!equal(mPath, logFile.getAbsolutePath())) { + BufferedWriter writer = mWriter; + if (writer != null) { + writer.close(); + } + mWriter = null; + mPath = null; + needCreate = true; + } + + BufferedWriter bufWriter = mWriter; + + if (needCreate || bufWriter == null) { + mPath = logFile.getAbsolutePath(); + FileWriter fileWriter = new FileWriter(logFile, true); + bufWriter = new BufferedWriter(fileWriter, BUFF_SIZE); + mWriter = bufWriter; + } + + // we can make FileWriter static, but when to close it + bufWriter.write(strLog); + + // It doesn't matter there are multiple files gets mixed. + final long curMillis = SystemClock.elapsedRealtime(); + if (curMillis - mLastMillis >= FLUSH_INTERVAL) { + bufWriter.flush(); + mLastMillis = curMillis; + } + + if (immediateClose) { + bufWriter.close(); + mPath = null; + mWriter = null; + } + } + } + + private static File createFile(String path, String fileName) { + return new File( + path.endsWith(File.separator) ? (path + fileName) : (path + + File.separator + fileName)); + } + + private static boolean equal(String s1, String s2) { + if (s1 != null && s2 != null) { + return s1.equals(s2); + } + return s1 == null && s2 == null; + } + + private static void deleteOldLogs() { + String dir = getLogPath(); + File dirFile = new File(dir); + if (!dirFile.exists()) { + return; + } + + long now = System.currentTimeMillis(); + File files[] = dirFile.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (isBakFile(file.getName())) { + long lastModifiedTime = file.lastModified(); + if (now - lastModifiedTime > DAY_DELAY) { + file.delete(); + } + } + } + } + + public static boolean getLogOutputPaths(MLog.LogOutputPaths out, String currentName) { + String dir = LogToES.getLogPath(); + if (dir == null || currentName == null) { + return false; + } + out.dir = dir; + String current = null; + synchronized (mLock) { + current = mPath; + } + if (current == null) { + current = createFile(dir, currentName).getAbsolutePath(); + } + out.currentLogFile = current; + + // get latest. + File folder = new File(dir); + File[] files = folder.listFiles(); + if (files != null) { + long maxModifiedTime = 0; + String dest = null; + for (File e : files) { + if (isBakFile(e.getAbsolutePath()) && e.lastModified() > maxModifiedTime) { + maxModifiedTime = e.lastModified(); + dest = e.getAbsolutePath(); + } + } + out.latestBackupFile = dest; + } + + return true; + } + + private static boolean isBakFile(String file) { + return file.endsWith(BAK_EXT); + } + + private static void limitVolume() { + String dir = getLogPath(); + File dirFile = new File(dir); + if (!dirFile.exists()) { + return; + } + + final File files[] = dirFile.listFiles(); + if (files == null || files.length <= Math.max(0, mBackFileNumLimit)) { + return; + } + + int numOfDeletable = 0; + for (int i = 0, n = files.length; i < n; i++) { + File file = files[i]; + if (isBakFile(file.getName())) { + ++numOfDeletable; + } + } + + if (numOfDeletable <= 0) { + // really weird, the naming rule have been changed! + // this function won't work anymore. + return; + } + + // the logs.txt and uncaught_exception.txt may be missing, + // so just allocate same size as the old. + File[] deletables = new File[numOfDeletable]; + int i = 0; + for (File e : files) { + if (i >= numOfDeletable) { + // unexpected case. + break; + } + + if (isBakFile(e.getName())) { + deletables[i++] = e; + } + } + + deleteIfOutOfBound(deletables); + } + + private static void deleteIfOutOfBound(File[] files) { + if (files.length <= mBackFileNumLimit) { + return; + } + + // sort files by create time(time is on the file name) DESC. + Comparator comparator = new Comparator() { + + @Override + public int compare(File lhs, File rhs) { + return rhs.getName().compareTo(lhs.getName()); + } + + }; + + Arrays.sort(files, comparator); + + final int filesNum = files.length; + + // delete files from index to size. + for (int i = mBackFileNumLimit; i < filesNum; ++i) { + File file = files[i]; + if (!file.delete()) { + // NOTE here we cannot call MLog, we are to be depended by MLog. + Log.e("LogToES", "LogToES failed to delete file " + file); + } + } + } + + public static void flush() { + synchronized (mLock) { + BufferedWriter writer = mWriter; + if (writer != null) { + try { + writer.flush(); + } catch (IOException e) { + MLog.error("LogToES", "flush error! " + e.toString()); + } + } + } + } + + public static void close() { + synchronized (mLock) { + BufferedWriter writer = mWriter; + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + MLog.error("LogToES", "close error! " + e.toString()); + } + } + mPath = null; + } + } + + public static boolean isOpen() { + synchronized (mLock) { + BufferedWriter writer = mWriter; + return writer != null; + } + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/log/MLog.java b/library/src/main/java/com/chwl/library/utils/log/MLog.java new file mode 100644 index 0000000..31f72bc --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/log/MLog.java @@ -0,0 +1,834 @@ +package com.chwl.library.utils.log; + +import android.os.Environment; +import android.os.Looper; +import android.os.Process; +import android.text.TextUtils; +import android.util.Log; + +import com.chwl.library.utils.FP; +import com.chwl.library.utils.LogCallerUtils; +import com.chwl.library.utils.config.BasicConfig; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collection; +import java.util.IllegalFormatException; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class MLog { + + private static final ExecutorService S_THREAD = Executors.newSingleThreadExecutor(); + private static volatile LogOptions sOptions = new LogOptions(); + + /** + * @param directory Where to put the logs folder. Should be a writable directory. + * @return True for succeeded, false otherwise. + */ + public static boolean initialize(String directory) { + return LogToES.setLogPath(directory); + } + + /** + * Get log output paths. + * + * @return null if not ready. + */ + public static LogOutputPaths getLogOutputPaths() { + LogOutputPaths ret = new LogOutputPaths(); + if (!getLogOutputPaths(ret)) { + MLog.error("MLog", "failed to get log output paths."); + } + return ret; + } + + /** + * Get log output paths. + * + * @param out Output destination. + * @return True for success, false otherwise. + */ + public static boolean getLogOutputPaths(LogOutputPaths out) { + return LogToES.getLogOutputPaths(out, sOptions.logFileName); + } + + /** + * @param directory Where to put the logs folder. + * @param options null-ok. Options for log methods. + * @return True for succeeded, false otherwise. + */ + public static boolean initialize(String directory, LogOptions options) { + setOptions(options); + return LogToES.setLogPath(directory); + } + + /** + * Make sure initialize is called before calling this. + */ + public static void setUniformTag(String tag) { + if (tag != null && tag.length() != 0) { + sOptions.uniformTag = tag; + } + } + + public static String getLogPath() { + return LogToES.getLogPath(); + } + + public static LogOptions getOptions() { + return sOptions; + } + + private static boolean setOptions(LogOptions options) { + final LogOptions tmpOp = (options == null ? new LogOptions() : options); + sOptions = tmpOp; + LogToES.setBackupLogLimitInMB(tmpOp.backUpLogLimitInMB); + LogToES.setBuffSize(tmpOp.buffSizeInBytes); + return tmpOp.buffSizeInBytes > 0 && !isNullOrEmpty(tmpOp.logFileName); + } + + /** + * Output verbose log. Exception will be caught if input arguments have + * format error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void verbose(Object obj, String format, Object... args) { + final boolean shouldOutputVerboseToDDMS = shouldOutputVerboseToDDMS(); + final boolean shouldOutputVerboseToFile = shouldOutputVerboseToFile(); + if (shouldOutputVerboseToDDMS || shouldOutputVerboseToFile) { + try { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + outputVerbose(obj, line, filename, format, shouldOutputVerboseToDDMS, shouldOutputVerboseToFile, args); + } catch (IllegalFormatException e) { + Log.e("MLog", "verbose fail, " + e); + } + } + } + + public static void verboseWithoutLineNumber(Object obj, String format, Object... args) { + final boolean shouldOutputVerboseToDDMS = shouldOutputVerboseToDDMS(); + final boolean shouldOutputVerboseToFile = shouldOutputVerboseToFile(); + if (shouldOutputVerboseToDDMS || shouldOutputVerboseToFile) { + try { + outputVerbose(obj, format, shouldOutputVerboseToDDMS, shouldOutputVerboseToFile, args); + } catch (IllegalFormatException e) { + Log.e("MLog", "verboseWithoutLineNumber fail, " + e); + } + } + } + + /** + * Output debug log. This version aims to improve performance by removing + * the string concatenated costs on release version. Exception will be + * caught if input arguments have format error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void debug(Object obj, String format, Object... args) { + if (shouldWriteDebug()) { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + outputDebug(obj, format, line, filename, args); + } + } + + public static void debugWithoutLineNumber(Object obj, String format, Object... args) { + if (shouldWriteDebug()) { + outputDebug(obj, format, args); + } + } + + /** + * Output information log. Exception will be caught if input arguments have + * format error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void info(Object obj, String format, Object... args) { + if (shouldWriteInfo()) { + try { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + outputInfo(obj, format, line, filename, args); + } catch (RuntimeException e) { + Log.e("MLog", "info fail, " + e); + } + } + } + + public static void infoWithoutLineNumber(Object obj, String format, Object... args) { + if (shouldWriteInfo()) { + try { + outputInfo(obj, format, args); + } catch (RuntimeException e) { + Log.e("MLog", "infoWithoutLineNumber fail, " + e); + } + } + } + + /** + * Output warning log. Exception will be caught if input arguments have + * format error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void warn(Object obj, String format, Object... args) { + if (shouldWriteWarn()) { + try { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + outputWarning(obj, format, line, filename, args); + } catch (RuntimeException e) { + Log.e("MLog", "warn fail, " + e); + } + } + } + + public static void warnWithoutLineNumber(Object obj, String format, Object... args) { + if (shouldWriteWarn()) { + try { + outputWarning(obj, format, args); + } catch (RuntimeException e) { + Log.e("MLog", "warnWithoutLineNumber fail, " + e); + } + } + } + + /** + * Output error log. Exception will be caught if input arguments have format + * error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void error(Object obj, String format, Object... args) { + if (shouldWriteError()) { + try { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + outputError(obj, format, line, filename, args); + } catch (RuntimeException e) { + Log.e("MLog", "error fail, " + e); + } + } + } + + public static void errorWithoutLineNumber(Object obj, String format, Object... args) { + if (shouldWriteError()) { + try { + outputError(obj, format, args); + } catch (RuntimeException e) { + Log.e("MLog", "errorWithoutLineNumber fail, " + e); + } + } + } + + /** + * Output an error log with contents of a Throwable. + * Exception will be caught if input arguments have format error. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param obj + * @param format The format string such as "This is the %d sample : %s". + * @param t An Throwable instance. + * @param args The args for format. + *

+ * Reference : boolean : %b. byte, short, int, long, Integer, Long + * : %d. NOTE %x for hex. String : %s. Object : %s, for this + * occasion, toString of the object will be called, and the + * object can be null - no exception for this occasion. + */ + public static void error(Object obj, String format, Throwable t, Object... args) { + error(obj, format + '\n' + stackTraceOf(t), args); + } + + public static void errorWithoutLineNumber(Object obj, String format, Throwable t, Object... args) { + errorWithoutLineNumber(obj, format + '\n' + stackTraceOf(t), args); + } + + /** + * Output an error log with contents of a Throwable. + *

+ * NOTE {@link #initialize(String)} or + * {@link #initialize(String, LogOptions)} must be called before calling + * this. + * + * @param t An Throwable instance. + */ + public static void error(Object obj, Throwable t) { + if (shouldWriteError()) { + int line = getCallerLineNumber(); + String filename = getCallerFilename(); + String methodname = getCallerMethodName(); + outputError(obj, t, line, filename, methodname); + } + } + + public static void errorWithoutLineNumber(Object obj, Throwable t) { + if (shouldWriteError()) { + String methodname = getCallerMethodName(); + outputError(obj, t, methodname); + } + } + + /** + * Flush the written logs. The log methods write logs to a buffer. + *

+ * NOTE this will be called if close is called. + */ + public static void flush() { + Runnable command = new Runnable() { + @Override + public void run() { + LogToES.flush(); + } + }; + + executeCommand(command); + } + + /** + * Close the logging task. Flush will be called here. Failed to call this + * may cause some logs lost. + */ + public static void close() { + Runnable command = new Runnable() { + @Override + public void run() { + if (externalStorageExist()) { + LogToES.close(); + } + } + }; + + executeCommand(command); + } + + public static boolean isOpen() { + return !S_THREAD.isShutdown() && !S_THREAD.isTerminated() && LogToES.isOpen(); + } + + private static void executeCommand(final Runnable command) { + S_THREAD.execute(command); + } + + private static String objClassName(Object obj) { + if (obj instanceof String) { + return (String) obj; + } + return obj.getClass().getSimpleName(); + } + + private static void writeToLog(final String logText) { + final long timeMillis = System.currentTimeMillis(); + final Runnable command = new Runnable() { + @Override + public void run() { + if (externalStorageExist()) { + try { + LogToES.writeLogToFile(LogToES.getLogPath(), + sOptions.logFileName, logText, false, timeMillis); + } catch (IOException e) { + Log.e("MLog", "writeToLog fail, " + e); + } + } + } + }; + executeCommand(command); + } + + private static void logToFile(String logText, Throwable t) { + StringWriter sw = new StringWriter(); + sw.write(logText); + sw.write("\n"); + t.printStackTrace(new PrintWriter(sw)); + writeToLog(sw.toString()); + } + + private static String msgForException(Object obj, String methodname, + String filename, int line) { + StringBuilder sb = new StringBuilder(); + if (obj instanceof String) { + sb.append((String) obj); + } else { + sb.append(obj.getClass().getSimpleName()); + } + sb.append(" Exception occurs at "); + sb.append("(P:"); + sb.append(Process.myPid()); + sb.append(")"); + sb.append("(T:"); + sb.append(Thread.currentThread().getId()); + sb.append(") at "); + sb.append(methodname); + sb.append(" ("); + sb.append(filename); + sb.append(":" + line); + sb.append(")"); + String ret = sb.toString(); + return ret; + } + + private static String msgForTextLog(Object obj, String filename, int line, + String msg) { + StringBuilder sb = new StringBuilder(); + + sb.append("["); + sb.append(objClassName(obj)); + sb.append("]"); + + sb.append(msg); + sb.append("(P:"); + sb.append(Process.myPid()); + sb.append(")"); + sb.append("(T:"); + + if (Looper.getMainLooper() == Looper.myLooper()) { + sb.append("Main"); + } else { + sb.append(Thread.currentThread().getId()); + } + + sb.append(")"); + + sb.append("at ("); + sb.append(filename); + sb.append(":"); + sb.append(line); + sb.append(")"); + String ret = sb.toString(); + return ret; + } + + private static int getCallerLineNumber() { + return Thread.currentThread().getStackTrace()[4].getLineNumber(); + } + + private static String getCallerFilename() { + return Thread.currentThread().getStackTrace()[4].getFileName(); + } + + private static String getCallerMethodName() { + return Thread.currentThread().getStackTrace()[4].getMethodName(); + } + + private static String getThreadStacksKeyword() { + return sOptions.stackTraceFilterKeyword; + } + + public static void printThreadStacks() { + printThreadStacks(tagOfStack(), getThreadStacksKeyword(), false, false); + } + + public static void printThreadStacks(String tag) { + printThreadStacks(tag, getThreadStacksKeyword(), + isNullOrEmpty(getThreadStacksKeyword()), false); + } + + public static void printThreadStacks(Throwable e, String tag) { + printStackTraces(e.getStackTrace(), tag); + } + + public static void printThreadStacks(String tag, String keyword) { + printThreadStacks(tag, keyword, false, false); + } + + // tag is for output identifier. + // keyword is for filtering irrelevant logs. + public static void printThreadStacks(String tag, String keyword, + boolean fullLog, boolean release) { + printStackTraces(Thread.currentThread().getStackTrace(), tag, keyword, + fullLog, release); + } + + public static void printStackTraces(StackTraceElement[] traces, String tag) { + printStackTraces(traces, tag, getThreadStacksKeyword(), + isNullOrEmpty(sOptions.stackTraceFilterKeyword), false); + } + + private static void printStackTraces(StackTraceElement[] traces, + String tag, String keyword, boolean fullLog, boolean release) { + printLog(tag, "------------------------------------", release); + for (StackTraceElement e : traces) { + String info = e.toString(); + if (fullLog || (!isNullOrEmpty(keyword) && info.indexOf(keyword) != -1)) { + printLog(tag, info, release); + } + } + printLog(tag, "------------------------------------", release); + } + + private static void printLog(String tag, String log, boolean release) { + if (release) { + info(tag, log); + } else { + debug(tag, log); + } + } + + public static String stackTraceOf(Throwable t) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + return sw.toString(); + } + + public static String stackTrace() { + StackTraceElement[] traces = Thread.currentThread().getStackTrace(); + return TextUtils.join("\n", traces); + } + + private static String tag(Object tag) { + final LogOptions options = sOptions; + return (options.uniformTag == null ? (tag instanceof String ? (String) tag + : tag.getClass().getSimpleName()) + : options.uniformTag); + } + + private static String tagOfStack() { + return (sOptions.uniformTag == null ? "CallStack" : sOptions.uniformTag); + } + + private static boolean shouldOutputVerboseToDDMS() { + return sOptions.logLevel <= LogOptions.LEVEL_VERBOSE; + } + + private static boolean shouldOutputVerboseToFile() { + return sOptions.logLevel <= LogOptions.LEVEL_VERBOSE && sOptions.honorVerbose; + } + + private static boolean shouldWriteDebug() { + return sOptions.logLevel <= LogOptions.LEVEL_DEBUG && BasicConfig.INSTANCE.isDebuggable(); + } + + private static boolean shouldWriteInfo() { + return sOptions.logLevel <= LogOptions.LEVEL_INFO && BasicConfig.INSTANCE.isDebuggable(); + } + + private static boolean shouldWriteWarn() { + return sOptions.logLevel <= LogOptions.LEVEL_WARN && BasicConfig.INSTANCE.isDebuggable(); + } + + private static boolean shouldWriteError() { + return sOptions.logLevel <= LogOptions.LEVEL_ERROR && BasicConfig.INSTANCE.isDebuggable(); + } + + private static boolean externalStorageExist() { + boolean isExternalStorageExist = false; + try { + isExternalStorageExist = Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED); + } catch (RuntimeException e) { + Log.e("MLog", e.toString()); + } + return isExternalStorageExist; + } + + private static boolean isNullOrEmpty(String s) { + return s == null || s.length() == 0; + } + + private static void outputVerbose(final Object obj, final String format, boolean outToDDMS, boolean outToFile, final Object... args) { + try { + String logText = (args == null || args.length == 0) ? format : String.format(format, args); + if (outToDDMS) { + Log.v(tag(obj), logText); + } + if (outToFile) { + writeToLog(logText); + } + } catch (RuntimeException e) { + Log.e("MLog", "outputVerbose fail, " + e); + } + } + + private static void outputVerbose(final Object obj, final int line, + final String filename, final String format, boolean outToDDMS, boolean outToFile, final Object... args) { + try { + String msg = (args == null || args.length == 0) ? format : String.format(format, args); + String logText = msgForTextLog(obj, filename, line, msg); + if (outToDDMS) { + Log.v(tag(obj), logText); + } + if (outToFile) { + writeToLog(logText); + } + } catch (RuntimeException e) { + Log.e("MLog", "outputVerbose fail, " + e); + } + } + + private static void outputDebug(final Object obj, final String format, final Object... args) { + try { + String logText = (args == null || args.length == 0) ? format : String.format(format, args); + Log.d(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputDebug fail, " + e); + } + } + + private static void outputDebug(final Object obj, final String format, + final int line, final String filename, final Object... args) { + try { + String msg = (args == null || args.length == 0) ? format : String.format(format, args); + String logText = msgForTextLog(obj, filename, line, msg); + Log.d(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputDebug fail, " + e); + } + } + + private static void outputInfo(final Object obj, final String format, final Object... args) { + try { + String logText = (args == null || args.length == 0) ? format : String.format(format, args); + Log.i(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputInfo fail, " + e); + } + } + + private static void outputInfo(final Object obj, final String format, + final int line, final String filename, final Object... args) { + try { + String msg = (args == null || args.length == 0) ? format : String.format(format, args); + String logText = msgForTextLog(obj, filename, line, msg); + Log.i(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputInfo fail, " + e); + } + } + + private static void outputWarning(final Object obj, final String format, final Object... args) { + try { + String logText = (args == null || args.length == 0) ? format : String.format(format, args); + Log.w(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputWarning fail, " + e); + } + } + + private static void outputWarning(final Object obj, final String format, + final int line, final String filename, final Object... args) { + try { + String msg = (args == null || args.length == 0) ? format : String.format(format, args); + String logText = msgForTextLog(obj, filename, line, msg); + Log.w(tag(obj), logText); + writeToLog(logText); + } catch (RuntimeException e) { + Log.e("MLog", "outputWarning fail, " + e); + } + } + + private static void outputError(final Object obj, final String format, final Object... args) { + try { + String logText = (args == null || args.length == 0) ? format : String.format(format, args); + // If the last arg is a throwable, print the stack. + if (args != null && args.length > 0 && args[args.length - 1] instanceof Throwable) { + Throwable t = (Throwable) args[args.length - 1]; + Log.e(tag(obj), logText, t); + logToFile(logText, t); + } else { + Log.e(tag(obj), logText); + writeToLog(logText); + } + } catch (RuntimeException e) { + Log.e("MLog", "outputError fail, " + e); + } + } + + private static void outputError(final Object obj, final String format, + final int line, final String filename, final Object... args) { + try { + String msg = (args == null || args.length == 0) ? format : String.format(format, args); + String logText = msgForTextLog(obj, filename, line, msg); + // If the last arg is a throwable, print the stack. + if (args != null && args.length > 0 && args[args.length - 1] instanceof Throwable) { + Throwable t = (Throwable) args[args.length - 1]; + Log.e(tag(obj), logText, t); + logToFile(logText, t); + } else { + Log.e(tag(obj), logText); + writeToLog(logText); + } + } catch (RuntimeException e) { + Log.e("MLog", "outputError fail, " + e); + } + } + + private static void outputError(final Object obj, final Throwable t, final String methodname) { + try { + String logText = objClassName(obj); + Log.e(tag(obj), logText, t); + logToFile(logText, t); + } catch (RuntimeException e) { + Log.e("MLog", "outputError fail, " + e); + } + + } + + private static void outputError(final Object obj, final Throwable t, + final int line, final String filename, final String methodname) { + try { + String logText = msgForException(obj, methodname, filename, line); + Log.e(tag(obj), logText, t); + logToFile(logText, t); + } catch (RuntimeException e) { + Log.e("MLog", "outputError fail, " + e); + } + } + + public static int getLogCollectionSize(Collection infos) { + return FP.empty(infos) ? 0 : infos.size(); + } + + public static int getLogMapSize(Map infos) { + return FP.empty(infos) ? 0 : infos.size(); + } + + public static void logStack(String msg) { + LogCallerUtils.logStack(msg); + } + + public static void logStack(String msg, int level) { + LogCallerUtils.logStack(msg, level); + } + + /** + * Log options. + */ + public static class LogOptions { + public static final int LEVEL_VERBOSE = 1; + public static final int LEVEL_DEBUG = 2; + public static final int LEVEL_INFO = 3; + public static final int LEVEL_WARN = 4; + public static final int LEVEL_ERROR = 5; + /** + * The level at which the log method really works(output to DDMS and + * file). + *

+ * NOTE this setting excludes the file writing of VERBOSE + * except when set {@link #honorVerbose} to true explicitly. + * If logLevel is LEVEL_VERBOSE: + * a) when honorVerbose is true, will output all logs to DDMS and file. + * b) when honorVerbose is false(default), will output all levels no less + * than LEVEL_DEBUG to DDMS and file, but for verbose, will only output + * to DDMS. + *

+ *

+ * MUST be one of the LEVEL_* constants. + */ + public int logLevel = LEVEL_VERBOSE; + /** + * Uniform tag to be used as log tag; null-ok, if this is null, will use + * the tag argument in log methods. + */ + public String uniformTag; + /** + * When it is null, all stack traces will be output. Usually this can be + * set the application package name. + */ + public String stackTraceFilterKeyword; + public boolean honorVerbose = false; + + /** + * Maximum backup log files' size in MB. Can be 0, which means no back + * up logs(old logs to be discarded). + */ + public int backUpLogLimitInMB = LogToES.DEFAULT_BAK_FILE_NUM_LIMIT + * LogToES.MAX_FILE_SIZE; + + /** + * Default file buffer size. Must be positive. + */ + public int buffSizeInBytes = LogToES.DEFAULT_BUFF_SIZE; + + /** + * Log file name, should not including the directory part. Must be a + * valid file name(for Android file system). + */ + public String logFileName = "logs.txt"; + } + + public static class LogOutputPaths { + /** + * The log directory, under which log files are put. + */ + public String dir; + + /** + * Current log file absolute file path. NOTE it may be empty. + */ + public String currentLogFile; + + /** + * Latest back up file path. null if there is none such file. + */ + public String latestBackupFile; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/net/MobileNumberUtils.java b/library/src/main/java/com/chwl/library/utils/net/MobileNumberUtils.java new file mode 100644 index 0000000..c65b4dd --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/net/MobileNumberUtils.java @@ -0,0 +1,70 @@ +package com.chwl.library.utils.net; + +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by levyyoung on 14-3-3. + */ +public class MobileNumberUtils { + + /* + * 国内各运营商手机号段 + * 移动: 134~139、147、150~152、157~159、182、183、187、188 + * 联通: 130~132、154、155、156、185、186 + * 电信: 133、153、189、180 + * */ + + //中移动 + private static final String REG_CHINA_MOBILE = "^1(3[4-9]|47|5[012789]|8[2378])\\d{8}$"; + //中联通 + private static final String REG_CHINA_UNICOM = "^1(3[0-2]|5[456]|8[56])\\d{8}$"; + //中电信 + private static final String REG_CHINA_TELECOM = "^1(33|53|8[019])\\d{8}$"; + //中国国内 + private static final String REG_CHINA_INTERNAL = "^1(3[0-9]|47|5[0-9]|8[023456789])\\d{8}$"; + + public static void hightLightChineseMobileNumber(int c, String text, TextView targetTv) { + String reg = "\\d{11}"; + Pattern pattern = Pattern.compile(reg); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + SpannableString s = new SpannableString(text); + Object blue = new ForegroundColorSpan(c); + s.setSpan(blue, matcher.start(), matcher.end(), Spannable.SPAN_MARK_POINT); + targetTv.setText(s); + } else { + targetTv.setText(text); + } + } + + public static boolean isChinaMobileNumber(CharSequence phoneNumber) { + Pattern pattern = Pattern.compile(REG_CHINA_MOBILE); + Matcher matcher = pattern.matcher(phoneNumber); + return matcher.matches(); + } + + public static boolean isChinaUnicomNumber(CharSequence phoneNumber) { + Pattern pattern = Pattern.compile(REG_CHINA_UNICOM); + Matcher matcher = pattern.matcher(phoneNumber); + return matcher.matches(); + } + + public static boolean isChinaTelecomNumber(CharSequence phoneNumber) { + Pattern pattern = Pattern.compile(REG_CHINA_TELECOM); + Matcher matcher = pattern.matcher(phoneNumber); + return matcher.matches(); + } + + public static boolean isChinaInternalNumber(CharSequence phoneNumber) { + Pattern pattern = Pattern.compile(REG_CHINA_INTERNAL); + Matcher matcher = pattern.matcher(phoneNumber); + return matcher.matches(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/net/NetworkMonitor.java b/library/src/main/java/com/chwl/library/utils/net/NetworkMonitor.java new file mode 100644 index 0000000..0f2894d --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/net/NetworkMonitor.java @@ -0,0 +1,98 @@ +package com.chwl.library.utils.net; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import com.chwl.library.utils.log.MLog; + +import java.util.concurrent.CopyOnWriteArraySet; + +public class NetworkMonitor extends BroadcastReceiver { + + /* Invalid network type. + * See #ConnectivityManager for other types. + */ + public static final int INVALID_TYPE = -1; + private static final NetworkMonitor S_INSTANCE = new NetworkMonitor(); + private CopyOnWriteArraySet mListener = new CopyOnWriteArraySet(); + + private NetworkMonitor() { + super(); + } + + public static NetworkMonitor instance() { + return S_INSTANCE; + } + + public void addListener(OnNetworkChange l) { + if (l != null) { + mListener.add(l); + } + } + + public void removeListener(OnNetworkChange l) { + mListener.remove(l); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mListener.isEmpty()) { + MLog.warn(this, "NetworkMonitor.onReceive, mListener is empty"); + return; + } + if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isAvailable()) { + Log.i("dingning", "NetworkMonitor.onReceive, disconnected"); + MLog.warn(this, "NetworkMonitor.onReceive, disconnected"); + for (OnNetworkChange l : mListener) { + l.onDisconnected(networkInfo != null ? networkInfo.getType() : INVALID_TYPE); + } + return; + } + int type = networkInfo.getType(); + if (networkInfo.isConnected()) { + Log.i("dingning", "NetworkMonitor.onReceive, connected, type = " + type); + MLog.warn(this, "NetworkMonitor.onReceive, connected, type = %d", type); + for (OnNetworkChange l : mListener) { + l.onConnected(type); + } + } else { + boolean connecting = networkInfo.isConnectedOrConnecting(); + if (connecting) { + Log.i("dingning", "NetworkMonitor.onReceive, connecting"); + MLog.warn(this, "NetworkMonitor.onReceive, connecting, type = %d", type); + for (OnNetworkChange l : mListener) { + l.onConnecting(type); + } + } else { + Log.i("dingning", "NetworkMonitor.onReceive, disconnected"); + MLog.warn(this, "NetworkMonitor.onReceive, disconnected, type = %d", type); + for (OnNetworkChange l : mListener) { + l.onDisconnected(type); + } + } + } + } + + } + + /** + * The type means network type. + */ + public static interface OnNetworkChange { + + void onDisconnected(int type); + + void onConnected(int type); + + void onConnecting(int type); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/pref/CommonPref.java b/library/src/main/java/com/chwl/library/utils/pref/CommonPref.java new file mode 100644 index 0000000..e4ea30a --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/pref/CommonPref.java @@ -0,0 +1,31 @@ +package com.chwl.library.utils.pref; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * + * Rule : input key cannot be null. + * + */ +public class CommonPref extends YSharedPref { + + private static volatile CommonPref sInst; + + private CommonPref(SharedPreferences preferences){ + super(preferences); + } + + public synchronized static CommonPref instance(Context applicationContext) { + if(sInst == null){ + synchronized (CommonPref.class) { + if (sInst == null) { + SharedPreferences pref = applicationContext.getApplicationContext() + .getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE); + sInst = new CommonPref(pref); + } + } + } + return sInst; + } +} diff --git a/library/src/main/java/com/chwl/library/utils/pref/ObjectPref.java b/library/src/main/java/com/chwl/library/utils/pref/ObjectPref.java new file mode 100644 index 0000000..ba5857d --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/pref/ObjectPref.java @@ -0,0 +1,27 @@ +package com.chwl.library.utils.pref; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.chwl.library.utils.config.BasicConfig; + +public class ObjectPref extends XSharedPref { + private static ObjectPref sInst; + + private ObjectPref(SharedPreferences preferences) { + super(preferences); + } + + public synchronized static ObjectPref instance(Context applicationContext) { + if (sInst == null) { + SharedPreferences pref = applicationContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE); + sInst = new ObjectPref(pref); + } + return sInst; + } + + public static ObjectPref instance() { + return instance(BasicConfig.INSTANCE.getAppContext()); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/pref/SettingsPref.java b/library/src/main/java/com/chwl/library/utils/pref/SettingsPref.java new file mode 100644 index 0000000..ac93b4b --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/pref/SettingsPref.java @@ -0,0 +1,42 @@ +package com.chwl.library.utils.pref; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.chwl.library.utils.config.BasicConfig; + +/** + * Creator: 舒强睿 + * Date:2015/1/12 + * Time:14:13 + *

+ * Description: + */ +public class SettingsPref extends YSharedPref { + + private static SettingsPref instance; + + private SettingsPref(SharedPreferences preferences) { + super(preferences); + } + + public synchronized static SettingsPref instance(Context applicationContext) { + if (instance == null) { + SharedPreferences pref = applicationContext.getSharedPreferences("SettingsPrefs", Context.MODE_PRIVATE); + instance = new SettingsPref(pref); + } + return instance; + } + + public static SettingsPref instance() { + return instance(BasicConfig.INSTANCE.getAppContext()); + } + + public SharedPreferences getSharePref() { + return mPref; + } + + public String getString(String key, String defaultValue) { + return get(key) == null ? defaultValue : get(key); + } +} diff --git a/library/src/main/java/com/chwl/library/utils/pref/XSharedPref.java b/library/src/main/java/com/chwl/library/utils/pref/XSharedPref.java new file mode 100644 index 0000000..bfc6235 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/pref/XSharedPref.java @@ -0,0 +1,327 @@ +package com.chwl.library.utils.pref; + +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; + +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.StreamCorruptedException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +public abstract class XSharedPref +{ + /** + * 保存在手机里面的文件名 + */ + protected static final String FILE_NAME = "share_data"; + + + protected final SharedPreferences mPref; + + public XSharedPref(SharedPreferences pref) { + mPref = pref; + } + + /** + * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法 + * + * @param key + * @param object + */ + public void put (String key, Object object) + { + + SharedPreferences.Editor editor = mPref.edit(); + + if (object instanceof String) + { + editor.putString(key, (String) object); + } else if (object instanceof Integer) + { + editor.putInt(key, (Integer) object); + } else if (object instanceof Boolean) + { + editor.putBoolean(key, (Boolean) object); + } else if (object instanceof Float) + { + editor.putFloat(key, (Float) object); + } else if (object instanceof Long) + { + editor.putLong(key, (Long) object); + } else + { + editor.putString(key, object.toString()); + } + + SharedPreferencesCompat.apply(editor); + } + + /** + * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值 + * + * @param key + * @param defaultObject + * @return + */ + public Object get(String key, Object defaultObject) + { + + if (defaultObject instanceof String) + { + return mPref.getString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) + { + return mPref.getInt(key, (Integer) defaultObject); + } else if (defaultObject instanceof Boolean) + { + return mPref.getBoolean(key, (Boolean) defaultObject); + } else if (defaultObject instanceof Float) + { + return mPref.getFloat(key, (Float) defaultObject); + } else if (defaultObject instanceof Long) + { + return mPref.getLong(key, (Long) defaultObject); + } + + return null; + } + + /** + * 移除某个key值已经对应的值 + * @param key + */ + public void remove( String key) + { + SharedPreferences.Editor editor = mPref.edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor); + } + + /** + * 清除所有数据 + */ + public void clear() + { + SharedPreferences.Editor editor = mPref.edit(); + editor.clear(); + SharedPreferencesCompat.apply(editor); + } + + /** + * 查询某个key是否已经存在 + * @param key + * @return + */ + public boolean contains(String key) + { + return mPref.contains(key); + } + + /** + * 返回所有的键值对 + * + * @return + */ + public Map getAll() + { + return mPref.getAll(); + } + + /** + * 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类 + * + * @author zhy + * + */ + private static class SharedPreferencesCompat + { + private static final Method sApplyMethod = findApplyMethod(); + + /** + * 反射查找apply的方法 + * + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Method findApplyMethod() + { + try + { + Class clz = SharedPreferences.Editor.class; + return clz.getMethod("apply"); + } catch (NoSuchMethodException e) + { + } + + return null; + } + + /** + * 如果找到则使用apply执行,否则使用commit + * + * @param editor + */ + public static void apply(SharedPreferences.Editor editor) + { + try + { + if (sApplyMethod != null) + { + sApplyMethod.invoke(editor); + return; + } + } catch (IllegalArgumentException e) + { + } catch (IllegalAccessException e) + { + } catch (InvocationTargetException e) + { + } + editor.commit(); + } + } + +// +// public static void setQuickReply(Context context, Object obj){ +// saveObject(context, "quickReply", obj); +// } +// +// public static ArrayList getQuickReply(Context context){ +// return (ArrayList) readObject(context,"quickReply"); +// } +// + + + /** + * desc:获取保存的Object对象 + * + * @param key + * @return modified: + */ + public Object readObject(String key) { + try { + if (mPref.contains(key)) { + String string = mPref.getString(key, ""); + if (TextUtils.isEmpty(string)) { + return null; + } else { + // 将16进制的数据转为数组,准备反序列化 + byte[] stringToBytes = StringToBytes(string); + ByteArrayInputStream bis = new ByteArrayInputStream(stringToBytes); + ObjectInputStream is = new ObjectInputStream(bis); + // 返回反序列化得到的对象 + Object readObject = is.readObject(); + return readObject; + } + } + } catch (StreamCorruptedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + // 所有异常返回null + return null; + } + + /** + * desc:将16进制的数据转为数组 + * + * @param data + * @return modified: + */ + private static byte[] StringToBytes(String data) { + String hexString = data.toUpperCase().trim(); + if (hexString.length() % 2 != 0) { + return null; + } + byte[] retData = new byte[hexString.length() / 2]; + for (int i = 0; i < hexString.length(); i++) { + int int_ch; // 两位16进制数转化后的10进制数 + char hex_char1 = hexString.charAt(i); // 两位16进制数中的第一位(高位*16) + int int_ch1; + if (hex_char1 >= '0' && hex_char1 <= '9') + int_ch1 = (hex_char1 - 48) * 16; // // 0 的Ascll - 48 + else if (hex_char1 >= 'A' && hex_char1 <= 'F') + int_ch1 = (hex_char1 - 55) * 16; // // A 的Ascll - 65 + else + return null; + i++; + char hex_char2 = hexString.charAt(i); // /两位16进制数中的第二位(低位) + int int_ch2; + if (hex_char2 >= '0' && hex_char2 <= '9') + int_ch2 = (hex_char2 - 48); // // 0 的Ascll - 48 + else if (hex_char2 >= 'A' && hex_char2 <= 'F') + int_ch2 = hex_char2 - 55; // // A 的Ascll - 65 + else + return null; + int_ch = int_ch1 + int_ch2; + retData[i / 2] = (byte) int_ch;// 将转化后的数放入Byte里 + } + return retData; + } + + /** + * desc:保存对象 + * + * @param key + * @param obj + * 要保存的对象,只能保存实现了serializable的对象 modified: + */ + public void saveObject(String key, Object obj) { + try { + // 保存对象 + SharedPreferences.Editor editor = mPref.edit(); + // 先将序列化结果写到byte缓存中,其实就分配一个内存空间 + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(bos); + // 将对象序列化写入byte缓存 + os.writeObject(obj); + // 将序列化的数据转为16进制保存 + String bytesToHexString = bytesToHexString(bos.toByteArray()); + // 保存该16进制数组 + editor.putString(key, bytesToHexString); + editor.commit(); + } catch (IOException e) { + e.printStackTrace(); + Log.e("", ResUtil.getString(R.string.utils_pref_xsharedpref_01)); + } + } + + /** + * desc:将数组转为16进制 + * + * @param bArray + * @return modified: + */ + private static String bytesToHexString(byte[] bArray) { + if (bArray == null) { + return null; + } + if (bArray.length == 0) { + return ""; + } + StringBuffer sb = new StringBuffer(bArray.length); + String sTemp; + for (int i = 0; i < bArray.length; i++) { + sTemp = Integer.toHexString(0xFF & bArray[i]); + if (sTemp.length() < 2) + sb.append(0); + sb.append(sTemp.toUpperCase()); + } + return sb.toString(); + } + + + +} diff --git a/library/src/main/java/com/chwl/library/utils/pref/YSharedPref.java b/library/src/main/java/com/chwl/library/utils/pref/YSharedPref.java new file mode 100644 index 0000000..67d2f7b --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/pref/YSharedPref.java @@ -0,0 +1,198 @@ +package com.chwl.library.utils.pref; + +import android.content.SharedPreferences; +import android.text.TextUtils; + +import com.chwl.library.utils.log.MLog; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * + * Rule : input key cannot be null. + * + */ +public abstract class YSharedPref { + /** + * 保存在手机里面的文件名 + */ + protected static final String FILE_NAME = "share_data"; + + private static final String DELIMITER = ","; + + protected final SharedPreferences mPref; + + public YSharedPref(SharedPreferences pref) { + mPref = pref; + } + + public void putString(String key, String value) { + put(key, value); + } + + public String getString(String key) { + return get(key); + } + + public void putInt(String key, int value) { + put(key, String.valueOf(value)); + } + + public void putBoolean(String key, boolean value) { + put(key, String.valueOf(value)); + } + + public boolean getBoolean(String key, boolean defaultValue) { + String rawValue = get(key); + if (TextUtils.isEmpty(rawValue)) { + return defaultValue; + } + try { + return Boolean.parseBoolean(rawValue); + } + catch (Exception e) { + MLog.error(this, "failed to parse boolean value for key %s, %s", key, e); + return defaultValue; + } + } + + public int getInt(String key, int defaultValue) { + String rawValue = get(key); + if (TextUtils.isEmpty(rawValue)) { + return defaultValue; + } + return parseInt(rawValue, defaultValue); + } + + private int parseInt(String value, int defaultValue) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + MLog.error(this, "lcy failed to parse value for key %s, %s", value, + e); + return defaultValue; + } + } + + public int getInt(String key) { + return getInt(key, -1); + } + + public void putLong(String key, long value) { + put(key, String.valueOf(value)); + } + + public long getLong(String key, long defaultValue) { + String rawValue = get(key); + if (TextUtils.isEmpty(rawValue)) { + return defaultValue; + } + try { + return Long.parseLong(rawValue); + } catch (NumberFormatException e) { + MLog.error(this, + "lcy failed to parse %s as long, for key %s, ex : %s", + rawValue, key, e); + return defaultValue; + } + } + + public long getLong(String key) { + return getLong(key, -1L); + } + + public void putIntArray(String key, Integer[] values) { + putIntList(key, Arrays.asList(values)); + } + + public int[] getIntArray(String key) { + return getIntArray(key, null); + } + + /** + * + * @param key + * @param outValues + * For memory reuse, if the result is no greater than this space, + * will fill into this, the redundant elements won't be touched. + * If it is null, a new int array will be created if result is + * not empty. + * @return The result list, null if no correlated. + */ + public int[] getIntArray(String key, int[] outValues) { + List list = getIntList(key); + if (list == null || list.size() == 0) { + return null; + } + + final int[] ret = (list.size() <= outValues.length) ? outValues + : new int[list.size()]; + + int i = 0; + for (Integer e : list) { + ret[i++] = e; + } + return ret; + + } + + public void putIntList(String key, List values) { + if (values == null || values.size() == 0) { + return; + } + + String value = TextUtils.join(DELIMITER, values); + put(key, value); + } + + public List getIntList(String key) { + String val = get(key); + if (TextUtils.isEmpty(val)) { + return null; + } + + String[] values = TextUtils.split(val, DELIMITER); + if (values == null || values.length == 0) { + return null; + } + + ArrayList list = new ArrayList(); + for (String e : values) { + try { + list.add(Integer.parseInt(e)); + } catch (NumberFormatException ex) { + MLog.error( + this, + "lcy failed to parse value for key: %s, value: %s, exception: %s", + key, e, ex); + continue; + } + + } + return list; + } + + final public void put(String key, String value) { + mPref.edit().putString(key, value).apply(); + } + + final public String get(String key) { + return mPref.getString(key, null); + } + + public void remove(String key){ + mPref.edit().remove(key).apply(); + } + + public void clear() { + mPref.edit().clear().apply(); + } + + public Map getAll(){ + return mPref.getAll(); + } + +} diff --git a/library/src/main/java/com/chwl/library/utils/valid/Validateable.java b/library/src/main/java/com/chwl/library/utils/valid/Validateable.java new file mode 100644 index 0000000..afb3e5d --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/valid/Validateable.java @@ -0,0 +1,6 @@ +package com.chwl.library.utils.valid; + +public interface Validateable { + + public abstract boolean validate(); +} diff --git a/library/src/main/java/com/chwl/library/utils/valid/Validation.java b/library/src/main/java/com/chwl/library/utils/valid/Validation.java new file mode 100644 index 0000000..02528e4 --- /dev/null +++ b/library/src/main/java/com/chwl/library/utils/valid/Validation.java @@ -0,0 +1,218 @@ +package com.chwl.library.utils.valid; + + +import com.chwl.library.utils.BlankUtil; +import com.chwl.library.utils.config.BasicConfig; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 基础断言类 + * + * @author 匡凌轩 V2.0 + */ +public class Validation { + + private static final String EXPRESSION_GREATER_EQUAL = ">="; + private static final String EXPRESSION_EQUAL = "="; + private static final String EXPRESSION_GREATER = ">"; + private static final String EXPRESSION_LESSER_EQUAL = "<="; + private static final String EXPRESSION_LESSER = "<"; + + /** + * 断言非空 + * @param dataName + * @param values + */ + public static void checkNull(String dataName, Object... values) { + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if(values == null){ + throw new IllegalArgumentException(dataName +" cannot be null"); + } + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + if(value == null){ + throw new IllegalArgumentException(dataName +" cannot be null at " + dataName + "[" + i + "]"); + } + } + } + + /** + * + * @param dataName + * @param expected + * @param value + */ + public static void checkEquals(String dataName, T expected, T value){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if(value == null || !expected.equals(value)){ + if(dataName == null){ + throw new IllegalArgumentException(value + " is expected to be equal to " + expected); + }else{ + throw new IllegalArgumentException(dataName + " is expected to be equal to " + value + " but in fact is " + expected); + } + } + } + + public static void checkEquals(T expected, T value){ + checkEquals(null, expected, value); + } + + public static void checkGreater(String dataName, Comparable value, T expected){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if(value == null || value.compareTo(expected) <= 0){ + if(dataName == null){ + throw new IllegalArgumentException(value + " is expected to be greater than " + expected); + }else{ + throw new IllegalArgumentException(dataName + " is expected to be greater than " + expected + " but in fact is " + value); + } + } + } + + public static void checkGreater(Comparable value, T expected){ + checkGreater(null, value, expected); + } + + public static void checkGreaterEquals(String dataName, Comparable value, T expected){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if(value == null || value.compareTo(expected) < 0){ + if(dataName == null){ + throw new IllegalArgumentException(value + " is expected to be greater equal than " + expected); + }else{ + throw new IllegalArgumentException(dataName + " is expected to be greater equal than " + expected + " but in fact is " + value); + } + } + } + + public static void checkGreaterEquals(Comparable value, T expected){ + checkGreaterEquals(null, value, expected); + } + + public static void checkLesser(String dataName, Comparable value, T expected){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if (value == null || value.compareTo(expected) >= 0) { + if(dataName == null){ + throw new IllegalArgumentException(value + " is expected to be lesser than " + expected); + }else{ + throw new IllegalArgumentException(dataName + " is expected to be lesser than " + expected + " but in fact is " + value); + } + } + } + + public static void checkLesser(Comparable value, T expected){ + checkLesser(null, value, expected); + } + + public static void checkLesserEquals(String dataName, Comparable value, T expected){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + if (value == null || value.compareTo(expected) > 0) { + if(dataName == null){ + throw new IllegalArgumentException(value + " is expected to be lesser equal than " + expected); + }else{ + throw new IllegalArgumentException(dataName + " is expected to be lesser equal than " + expected + " but in fact is " + value); + } + } + } + + public static void checkLesserEquals(Comparable value, T expected){ + checkLesserEquals(null, value, expected); + } + + public static void checkNumeric(String dataName, Object... values){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + checkNull(dataName, values); + for (Object value : values) { + checkNull(dataName, value); + checkRegex(dataName, "[-+]?[0-9]+", value.toString()); + } + } + + /** + * 表达式组合验证,支持 =,>=,<,>,<= + * Validation.check("=", 6, 6);//x == 6 + * Validation.check(">,<", 15, 10, 9, 10);// x > 10 && x < 10 + */ + public static void check(String expression, Comparable... values){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + String[] exps = expression.split(","); + if(values.length % 2 != 0){ + throw new IllegalArgumentException("check failed cause by values is not a pair : "+ values.length); + } + for (int i = 0; i < values.length; ) { + for (String exp : exps) { + if (BlankUtil.isBlank(exp)) { + throw new IllegalArgumentException( + "check failed cause by expression : " + expression); + } + Comparable value1 = values[i++]; + T value2 = (T) values[i++]; + if (EXPRESSION_EQUAL.equals(exp)) { + checkEquals(value1, value2); + } else if (EXPRESSION_GREATER.equals(exp)) { + checkGreater(value1, value2); + } else if (EXPRESSION_GREATER_EQUAL.equals(exp)) { + checkGreaterEquals(value1, value2); + } else if (EXPRESSION_LESSER.equals(exp)) { + checkLesser(value1, value2); + } else if (EXPRESSION_LESSER_EQUAL.equals(exp)) { + checkLesserEquals(value1, value2); + } + } + } + } + + /** + * 正则表达式验证 + * @param regex + * @param value + */ + public static void checkRegex(String dataName, String regex, String value){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + Pattern pattern = Pattern.compile(regex); + Matcher isNum = pattern.matcher(value); + if( !isNum.matches() ) + { + throw new IllegalArgumentException("check failed cause by " + dataName + " is not number : "+value); + } + } + + /** + * Bean需要实现Validateable的validate方法 + * @param bean + */ + public static void checkBean(Validateable bean){ + if (!BasicConfig.INSTANCE.isDebuggable()) { + return; + } + + bean.validate(); + } +} diff --git a/library/src/main/java/com/chwl/library/widget/DrawableCenterTextView.java b/library/src/main/java/com/chwl/library/widget/DrawableCenterTextView.java new file mode 100644 index 0000000..72dacfc --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/DrawableCenterTextView.java @@ -0,0 +1,40 @@ +package com.chwl.library.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; + +import com.chwl.library.utils.CenterDrawableHelper; + + +/** + *

+ * + * @author jiahui + * @date 2018/1/11 + */ +public class DrawableCenterTextView extends androidx.appcompat.widget.AppCompatTextView { + + + public DrawableCenterTextView(Context context) { + super(context); + } + + public DrawableCenterTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DrawableCenterTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + + @Override + protected void onDraw(Canvas canvas) { + CenterDrawableHelper.preDraw(this, canvas); + super.onDraw(canvas); + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/IOSSwitchView.java b/library/src/main/java/com/chwl/library/widget/IOSSwitchView.java new file mode 100644 index 0000000..e42a088 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/IOSSwitchView.java @@ -0,0 +1,571 @@ +package com.chwl.library.widget; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Property; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.chwl.library.R; + +/** + * 高仿IOS风格的UISwitchView + * Created by HanHailong on 15/10/15. + * copy from https://github.com/hanhailong/IOSSwitchView + */ +public class IOSSwitchView extends View { + + private static final int foregroundColor = 0xFFEFEFEF; + private static final int backgroundColor = 0xFFCCCCCC; + + private int colorStep = backgroundColor; + private int mTintColor; + private int mThumbTintColor; + private int mStrokeWidth; + + private boolean isOn = false; + private boolean preIsOn; + private boolean thumbState; + + private int width; + private int height; + + private Paint mPaint; + + private ObjectAnimator mInnerContentAnimator; + private ObjectAnimator mThumbExpandAnimator; + private ObjectAnimator mThumbMoveAnimator; + + private OnSwitchStateChangeListener mOnSwitchStateChangeListener; + + private float innerContentRate = 1.0f; + private float thumbExpandRate; + private float thumbMoveRate; + + private RectF innerContentRectF; + private RectF thumbRectF; + private RectF tempRoundRectF; + + private float cornerRadius; + private float centerX; + private float centerY; + private float intrinsicInnerWidth; + private float intrinsicInnerHeight; + private float intrinsicThumbWidth; + private float thumbMaxExpandWidth; + + //手势检测器 + private GestureDetector mGestureDetector; + + public IOSSwitchView(Context context) { + this(context, null); + } + + public IOSSwitchView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public IOSSwitchView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + /** + * 初始化 + * + * @param context + * @param attrs + */ + private void init(Context context, AttributeSet attrs) { + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.IOSSwitchView); + mTintColor = a.getColor(R.styleable.IOSSwitchView_tintColor, getSelectedColor()); + mThumbTintColor = a.getColor(R.styleable.IOSSwitchView_thumbTintColor, Color.WHITE); + + int defaultStrokeWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.5f, context.getResources() + .getDisplayMetrics()); + mStrokeWidth = a.getDimensionPixelOffset(R.styleable.IOSSwitchView_strokeWidth, defaultStrokeWidth); + isOn = a.getBoolean(R.styleable.IOSSwitchView_isOn, false); + preIsOn = isOn; + thumbState = isOn; + + if (isOn) { + thumbMoveRate = 1.0f; + innerContentRate = 0.0f; + } else { + thumbMoveRate = 0.0f; + innerContentRate = 1.0f; + } + + a.recycle(); + + //初始化画笔、动画等属性 + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + tempRoundRectF = new RectF(); + innerContentRectF = new RectF(); + thumbRectF = new RectF(); + + //启动软加速 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB) + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + //灰白色矩形形变动画 + mInnerContentAnimator = ObjectAnimator.ofFloat(this, new Property(Float.class, "innerbound") { + @Override + public void set(IOSSwitchView object, Float value) { + object.setInnerContentRate(value); + } + + @Override + public Float get(IOSSwitchView object) { + return object.getInnerContentRate(); + } + }, innerContentRate, 1.0f); + mInnerContentAnimator.setDuration(300); + mInnerContentAnimator.setInterpolator(new DecelerateInterpolator()); + + //thumb拉伸动画 + mThumbExpandAnimator = ObjectAnimator.ofFloat(this, new Property(Float.class, "thumbExpand") { + @Override + public void set(IOSSwitchView object, Float value) { + object.setThumbExpandRate(value); + } + + @Override + public Float get(IOSSwitchView object) { + return object.getThumbExpandRate(); + } + }, thumbExpandRate, 1.0f); + mThumbExpandAnimator.setDuration(300); + mThumbExpandAnimator.setInterpolator(new DecelerateInterpolator()); + + //thumb位移动画 + mThumbMoveAnimator = ObjectAnimator.ofFloat(this, new Property(Float.class, "thumbMove") { + @Override + public void set(IOSSwitchView object, Float value) { + object.setThumbMoveRate(value); + } + + @Override + public Float get(IOSSwitchView object) { + return object.getThumbMoveRate(); + } + }, thumbMoveRate, 1.0f); + mThumbMoveAnimator.setDuration(300); + mThumbMoveAnimator.setInterpolator(new DecelerateInterpolator()); + + //手势 + mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDown(MotionEvent e) { + + if (!isEnabled()) return false; + + preIsOn = isOn; + + //灰白色矩形缩小到0 + mInnerContentAnimator.setFloatValues(innerContentRate, 0.0f); + mInnerContentAnimator.start(); + + //thumb有个拉伸的动作 + mThumbExpandAnimator.setFloatValues(thumbExpandRate, 1.0f); + mThumbExpandAnimator.start(); + + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + //手指抬起执行一系列的动画 + isOn = thumbState; + + if (preIsOn == isOn) {//反转 + isOn = !isOn; + thumbState = !thumbState; + } + + //打开状态 + if (thumbState) { + //thumb移动到右侧打开区域 + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 1.0F); + mThumbMoveAnimator.start(); + + //灰白色圆角矩形缩小到0 + mInnerContentAnimator.setFloatValues(innerContentRate, 0.0F); + mInnerContentAnimator.start(); + } else {//关闭状态 + //thumb移动到左侧关闭区域 + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 0.0F); + mThumbMoveAnimator.start(); + + //灰白色圆角矩形放大到覆盖背景大小 + mInnerContentAnimator.setFloatValues(innerContentRate, 1.0F); + mInnerContentAnimator.start(); + } + //thumb恢复原大小 + mThumbExpandAnimator.setFloatValues(thumbExpandRate, 0.0F); + mThumbExpandAnimator.start(); + + if (mOnSwitchStateChangeListener != null && preIsOn != isOn) { + mOnSwitchStateChangeListener.onStateSwitched(isOn); + } + + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + //在打开开关的区域 + if (e2.getX() > centerX) { + //并且开关状态是关闭的,就执行打开开关操作 + if (!thumbState) { + thumbState = !thumbState; + + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 1.0F); + mThumbMoveAnimator.start(); + + mInnerContentAnimator.setFloatValues(innerContentRate, 0.0F); + mInnerContentAnimator.start(); + } + } else {//在关闭区域 + //开关处于打开状态 + if (thumbState) { + thumbState = !thumbState; + //执行关闭开关动画 + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 0.0F); + mThumbMoveAnimator.start(); + } + } + + return true; + } + + }); + //禁止长按 + mGestureDetector.setIsLongpressEnabled(false); + + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + //测量宽度和高度 + width = MeasureSpec.getSize(widthMeasureSpec); + height = MeasureSpec.getSize(heightMeasureSpec); + + //保持一定的宽高比例 + + if (mIsKeepRate && ((float) height / (float) width < 0.5f)) { + height = (int) (width * 0.5); + + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)); + super.setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); + } + + centerX = width * 0.5f; + centerY = height * 0.5f; + cornerRadius = centerY; + + innerContentRectF.left = mStrokeWidth; + innerContentRectF.top = mStrokeWidth; + innerContentRectF.right = width - mStrokeWidth; + innerContentRectF.bottom = height - mStrokeWidth; + + intrinsicInnerWidth = innerContentRectF.width(); + intrinsicInnerHeight = innerContentRectF.height(); + + thumbRectF.left = mStrokeWidth; + thumbRectF.top = mStrokeWidth; + thumbRectF.right = width - mStrokeWidth; + thumbRectF.bottom = height - mStrokeWidth; + + intrinsicThumbWidth = thumbRectF.height(); + + //thumb最大拉伸宽度 + thumbMaxExpandWidth = width * 0.7f; + + if (thumbMaxExpandWidth > intrinsicThumbWidth * 1.25f) { + thumbMaxExpandWidth = intrinsicThumbWidth * 1.25f; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + + if (!thumbState) { + mInnerContentAnimator.setFloatValues(innerContentRate, 1.0f); + mInnerContentAnimator.start(); + } + + mThumbExpandAnimator.setFloatValues(thumbExpandRate, 0.0f); + mThumbExpandAnimator.start(); + + isOn = thumbState; + + if (mOnSwitchStateChangeListener != null && isOn != preIsOn) { + mOnSwitchStateChangeListener.onStateSwitched(isOn); + } + + break; + } + + return mGestureDetector.onTouchEvent(event); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float w = intrinsicInnerWidth * 0.5f * innerContentRate; + float h = intrinsicInnerHeight * 0.5f * innerContentRate; + + this.innerContentRectF.left = centerX - w; + this.innerContentRectF.top = centerY - h; + this.innerContentRectF.right = centerX + w; + this.innerContentRectF.bottom = centerY + h; + + //thumb拉伸宽度变化,其变化值从1->1.7之间 + w = intrinsicThumbWidth + (thumbMaxExpandWidth - intrinsicThumbWidth) * thumbExpandRate; + + boolean left = thumbRectF.left + thumbRectF.width() * 0.5f < centerX; + if (left) { + thumbRectF.left = thumbRectF.right - w; + } else { + thumbRectF.right = thumbRectF.left + w; + } + + float kw = thumbRectF.width(); + w = (float) (width - kw - (mStrokeWidth * 2)) * thumbMoveRate; + + thumbRectF.left = mStrokeWidth + w; + thumbRectF.right = thumbRectF.left + kw; + + //颜色值过渡变化,从深灰白色变化到tintColor色 + if (mIsNoStrokeColor) + this.colorStep = transformRGBColor(thumbMoveRate, foregroundColor, mTintColor); + else + this.colorStep = transformRGBColor(thumbMoveRate, backgroundColor, mTintColor); + + //画TintColor颜色的圆角矩形 + mPaint.setColor(colorStep); + mPaint.setStyle(Paint.Style.FILL); + drawRoundRect(0, 0, width, height, cornerRadius, canvas, mPaint); + + mPaint.setColor(foregroundColor); + //画灰白色圆角矩形 + canvas.drawRoundRect(innerContentRectF, innerContentRectF.height() * 0.5f, innerContentRectF.height() * 0.5f, mPaint); + + //画thumb + mPaint.setColor(mThumbTintColor); + canvas.drawRoundRect(thumbRectF, cornerRadius, cornerRadius, mPaint); + + mPaint.setColor(foregroundColor); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(1); + canvas.drawRoundRect(thumbRectF, cornerRadius, cornerRadius, mPaint); + } + + /** + * RGB颜色过渡变化 + * + * @param progress + * @param fromColor + * @param toColor + * @return + */ + private int transformRGBColor(float progress, int fromColor, int toColor) { + int fr = (fromColor >> 16) & 0xFF; + int fg = (fromColor >> 8) & 0xFF; + int fb = fromColor & 0xFF; + + int tr = (toColor >> 16) & 0xFF; + int tg = (toColor >> 8) & 0xFF; + int tb = toColor & 0xFF; + + int rGap = (int) ((float) (tr - fr) * progress); + int gGap = (int) ((float) (tg - fg) * progress); + int bGap = (int) ((float) (tb - fb) * progress); + + return 0xFF000000 | ((fr + rGap) << 16) | ((fg + gGap) << 8) | (fb + bGap); + } + + /** + * 画圆角矩形 + * + * @param left + * @param top + * @param right + * @param bottom + * @param radius + * @param canvas + * @param paint + */ + private void drawRoundRect(float left, float top, float right, float bottom, float radius, Canvas canvas, Paint paint) { + tempRoundRectF.set(left, top, right, bottom); + canvas.drawRoundRect(tempRoundRectF, radius, radius, paint); + } + + /** + * 设置切换状态变化监听器 + * + * @param listener + */ + public void setOnSwitchStateChangeListener(OnSwitchStateChangeListener listener) { + this.mOnSwitchStateChangeListener = listener; + } + + public int getTintColor() { + return mTintColor; + } + + public void setTintColor(int mTintColor) { + this.mTintColor = mTintColor; + } + + public int getThumbTintColor() { + return mThumbTintColor; + } + + public void setThumbTintColor(int mThumbTintColor) { + this.mThumbTintColor = mThumbTintColor; + } + + public int getStrokeWidth() { + return mStrokeWidth; + } + + public void setStrokeWidth(int mStrokeWidth) { + this.mStrokeWidth = mStrokeWidth; + } + + public boolean isOn() { + return isOn; + } + + /** + * 设置是否选中 + * + * @param on + */ + public void setOn(boolean on) { + setOn(on, false); + } + public void setOn(boolean on, boolean isNeedListener) { + setOn(on, false, isNeedListener); + } + public void setOn(boolean on, boolean animate, boolean isNeedListener) { + if (isOn == on) return; + + isOn = on; + thumbState = on; + + //有动画效果 + if (animate) { + if (on) { + mInnerContentAnimator.setFloatValues(innerContentRate, 0); + mInnerContentAnimator.start(); + + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 1); + mThumbMoveAnimator.start(); + } else { + mInnerContentAnimator.setFloatValues(innerContentRate, 1); + mInnerContentAnimator.start(); + + mThumbMoveAnimator.setFloatValues(thumbMoveRate, 0); + mThumbMoveAnimator.start(); + } + + mThumbExpandAnimator.setFloatValues(thumbExpandRate, 0); + mThumbExpandAnimator.start(); + } else { + //设置选中 + if (on) { + setThumbMoveRate(1); + setInnerContentRate(0); + } else { + setThumbMoveRate(0); + setInnerContentRate(1); + } + setThumbExpandRate(0); + } + + if (mOnSwitchStateChangeListener != null && isNeedListener) { + mOnSwitchStateChangeListener.onStateSwitched(isOn); + } + } + + private float getInnerContentRate() { + return innerContentRate; + } + + private void setInnerContentRate(float innerContentRate) { + this.innerContentRate = innerContentRate; + invalidate(); + } + + private float getThumbExpandRate() { + return thumbExpandRate; + } + + private void setThumbExpandRate(float thumbExpandRate) { + this.thumbExpandRate = thumbExpandRate; + invalidate(); + } + + private float getThumbMoveRate() { + return thumbMoveRate; + } + + private void setThumbMoveRate(float thumbMoveRate) { + this.thumbMoveRate = thumbMoveRate; + invalidate(); + } + + private boolean mIsNoStrokeColor = false; + public void setNoStrokeColor(boolean isNoStrokeColor) { + mIsNoStrokeColor = isNoStrokeColor; + invalidate(); + } + + private boolean mIsKeepRate = true; + public void setmIsKeepRate(boolean isKeepRate) { + mIsKeepRate = isKeepRate; + invalidate(); + } + + public int getSelectedColor() { + return 0xFFFF894F; + } + + /** + * SwitchView状态切换 + */ + public static interface OnSwitchStateChangeListener { + /** + * 是否选中 + * + * @param isOn + */ + public void onStateSwitched(boolean isOn); + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/SVGAView.kt b/library/src/main/java/com/chwl/library/widget/SVGAView.kt new file mode 100644 index 0000000..28b2036 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/SVGAView.kt @@ -0,0 +1,435 @@ +package com.chwl.library.widget + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.graphics.drawable.AnimationDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.LruCache +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.chwl.library.common.glide.GlideUtils +import com.chwl.library.common.util.isVerify +import com.chwl.library.utils.LogUtil +import com.example.lib_utils.AppUtils +import com.example.lib_utils.log.ILog +import com.opensource.svgaplayer.SVGADrawable +import com.opensource.svgaplayer.SVGAImageView +import com.opensource.svgaplayer.SVGAParser +import com.opensource.svgaplayer.SVGAParser.Companion.shareParser +import com.opensource.svgaplayer.SVGAVideoEntity +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.net.MalformedURLException + +class SVGAView : SVGAImageView, ILog { + companion object { + val shareParser: SVGAParser by lazy { + SVGAParser(AppUtils.getApp()) + } + + @JvmStatic + fun newCache(maxSize: Int): SVGACache { + if (maxSize > 1) { + return SVGALruCache(maxSize) + } else { + return SVGASingleCache() + } + } + } + + var resumePlayAfterAttached = true + + private var resourceUrl: String? = null + + private var svgaCache: SVGACache? = null + + private val downloadTag: String get() = "SVGAVIEW_${hashCode()}" + + private var logTag: String? = null + + + private var mNeedLog = false + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + fun loadFileHasCallBack(name: String,callBack : (isSuccess:Boolean) -> Unit) { + try { + this.setImageDrawable(null) + this.resourceUrl = name + val cacheItem = svgaCache?.get(name) + if (cacheItem != null) { + logD("SVGAView loadUrl() 有缓存") + this@SVGAView.post { + this@SVGAView.setImageDrawable(SVGADrawable(cacheItem)) + this@SVGAView.startAnimation() + callBack(true) + } + return + } + + SVGAParser.shareParser() + .decodeFromAssets(name, object : SVGAParser.ParseCompletion { + override fun onComplete(videoItem: SVGAVideoEntity) { + svgaCache?.put(name,videoItem) + val drawable = SVGADrawable(videoItem) + setImageDrawable(drawable) + startAnimation() + callBack(true) + } + + override fun onError() { + LogUtil.print("onError") + callBack(false) + } + }) + } catch (e: MalformedURLException) { + + } + } + + + fun loadFile(name: String) { + try { + this.setImageDrawable(null) + this.resourceUrl = name + val cacheItem = svgaCache?.get(name) + if (cacheItem != null) { + logD("SVGAView loadUrl() 有缓存") + this@SVGAView.post { + this@SVGAView.setImageDrawable(SVGADrawable(cacheItem)) + this@SVGAView.startAnimation() + } + return + } + + SVGAParser.shareParser() + .decodeFromAssets(name, object : SVGAParser.ParseCompletion { + override fun onComplete(videoItem: SVGAVideoEntity) { + svgaCache?.put(name,videoItem) + val drawable = SVGADrawable(videoItem) + setImageDrawable(drawable) + startAnimation() + } + + override fun onError() { + LogUtil.print("onError") + } + }) + } catch (e: MalformedURLException) { + + } + } + + fun loadUrl(url: String?,autoPayer:Boolean = true) { + logD("SVGAView loadUrl() url:$url") + if (url.isNullOrEmpty()) { + this.resourceUrl = null + this.setImageDrawable(null) + onViewStateChanged(0) + return + } + + if (url == resourceUrl && drawable is SVGADrawable) { + logD("SVGAView loadUrl() 已加载 isAnimating:$isAnimating") + if (!isAnimating) { + if (autoPayer) { + startAnimation() + } + } + return + } + + this.setImageDrawable(null) + this.resourceUrl = url + val cacheItem = svgaCache?.get(url) + if (cacheItem != null) { + logD("SVGAView loadUrl() 有缓存") + this@SVGAView.post { + this@SVGAView.setImageDrawable(SVGADrawable(cacheItem)) + if (autoPayer) { + this@SVGAView.startAnimation() + } + } + return + } + loadSVGAUrl(url,autoPayer) + } + + private fun loadSVGAUrl(url: String,autoPayer:Boolean = true) { + logD("SVGAView loadSVGAUrl url:$url") + GlideUtils.instance().downloadFromUrl2(context,url,object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + logD("SVGAView loadSVGAUrl onDownloadError url:$url") + if (resourceUrl == url) { + onViewStateChanged(-1) + } + return true + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + if (resource != null) { + val path = resource.path + if (resourceUrl == url) { + var isImg = false + var outMimeType = "null" + try { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + val decodeFile = BitmapFactory.decodeFile(path, options) + outMimeType = options.outMimeType + isImg = true + + } catch (e: Exception) { + isImg = false + } + + logD("SVGAView loadSVGAUrl onDownloadCompleted url:$url isImg=$isImg outMimeType=$outMimeType path:$path") + if (!isImg) { + loadSVGAFile(url, path, autoPayer) + } else { + this@SVGAView.post { + this@SVGAView.resourceUrl = null + onViewStateChanged(0) + loadImage(url) + } + } + + } + } + return true + } + }) + } + + private fun loadImage(url: String) { + Glide.with(this) + .asBitmap() + .dontAnimate() + .dontTransform() + .load(url) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + this@SVGAView.resourceUrl = null + onViewStateChanged(0) + return false + } + + override fun onResourceReady( + resource: Bitmap?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + + if (resource == null) return false + val split: List = split(resource) + this@SVGAView.post { + if (split.isVerify()) { + val animationDrawable = AnimationDrawable() + for (i in split.indices) { + animationDrawable.addFrame(split[i], 200) + } + this@SVGAView.post(Runnable { + this@SVGAView.setImageDrawable(animationDrawable) + animationDrawable.isOneShot = false + animationDrawable.start() + }) + } else { + setImageBitmap(resource) + } + } + return false + } + }).submit() + } + + private fun split(bitmap: Bitmap?): List { + val pieces: MutableList = ArrayList() + try { + if (bitmap!!.width == bitmap.height) { + pieces.add(BitmapDrawable(bitmap)) + } else { + val matrix = Matrix() + matrix.setScale(0.5f, 0.5f) + + val width = bitmap.width + val pieceWidth = bitmap.height + val pieceHeight = bitmap.height + val xPiece = width / pieceWidth + for (j in 0 until xPiece) { + val xValue = j * pieceWidth + pieces.add( + BitmapDrawable( + Bitmap.createBitmap( + bitmap, xValue, 0, + pieceWidth, pieceHeight, matrix, true + ) + ) + ) + } + } + } catch (e: java.lang.Exception) { + } + return pieces + } + + private fun loadSVGAFile(url: String, path: String,autoPayer:Boolean = true) { + try { + logD("SVGAView loadSVGAFile path:$path url:$url") + val inputStream = BufferedInputStream(FileInputStream(path)) + shareParser().decodeFromInputStream( + inputStream, + path, + object : SVGAParser.ParseCompletion { + override fun onComplete(videoItem: SVGAVideoEntity) { + logD("SVGAView parseCompletion onComplete url:$url") + if (resourceUrl != url) { + return + } + svgaCache?.put(resourceUrl ?: "", videoItem) + this@SVGAView.post { + this@SVGAView.setImageDrawable(SVGADrawable(videoItem)) + if (autoPayer) { + this@SVGAView.startAnimation() + } + onViewStateChanged(1) + } + } + + override fun onError() { + logD("SVGAView parseCompletion onError url:$url") + if (resourceUrl != url) { + return + } + onViewStateChanged(-1) + } + }, + true, + null, + null + ) + } catch (e: Exception) { + e.printStackTrace() + logD("SVGAView loadSVGAFile url:$url e:${e.message}") + onViewStateChanged(-1) + } + } + + /** + * @param state -1 异常、0 空、1 成功 + */ + private fun onViewStateChanged(state: Int) { + logD("SVGAView onViewStateChanged state:$state") + } + + fun bindCache(cache: SVGACache?) { + logD("SVGAView bindCache() cache:$cache") + this.svgaCache = cache + } + + fun setLogTag(tag: String) { + logD("SVGAView setLogTag() newTag:$tag oldTag:$logTag") + this.logTag = tag + } + + override fun logD(message: String, tag: String, filePrinter: Boolean) { + if (!mNeedLog) return + if (logTag != null) { + super.logD("#$logTag# $message", tag, filePrinter) + } else { + super.logD(message, tag, filePrinter) + } + } + + override fun onDetachedFromWindow() { + logD("SVGAView onDetachedFromWindow()") + super.onDetachedFromWindow() + } + + override fun onAttachedToWindow() { + logD("SVGAView onAttachedToWindow()") + super.onAttachedToWindow() + if (resumePlayAfterAttached) { + if (drawable is SVGADrawable) { + if (!isAnimating) { + logD("SVGAView onAttachedToWindow() startAnimation") + startAnimation() + } + } + } + } + + + + interface SVGACache { + fun get(key: String): SVGAVideoEntity? + + fun put(key: String, entity: SVGAVideoEntity) + + fun clear() + } + + class SVGALruCache(maxSize: Int) : SVGACache { + val lruCache = LruCache(maxSize) + override fun get(key: String): SVGAVideoEntity? { + return lruCache.get(key) + } + + override fun put(key: String, entity: SVGAVideoEntity) { + lruCache.put(key, entity) + } + + override fun clear() { + lruCache.evictAll() + } + } + + class SVGASingleCache : SVGACache { + var cache: Pair? = null + override fun get(key: String): SVGAVideoEntity? { + if (cache?.first == key) { + return cache?.second + } + return null + } + + override fun put(key: String, entity: SVGAVideoEntity) { + cache = Pair(key, entity) + } + + override fun clear() { + cache = null + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/ShapeConstrainLayout.java b/library/src/main/java/com/chwl/library/widget/ShapeConstrainLayout.java new file mode 100644 index 0000000..68a6294 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/ShapeConstrainLayout.java @@ -0,0 +1,72 @@ +package com.chwl.library.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.chwl.library.R; + +/** + * 带圆角的约束布局 + * Created by lvzebiao on 2019/1/7. + */ + +public class ShapeConstrainLayout extends ConstraintLayout{ + + private int scaleWidth; + + private int scaleHeight; + + public ShapeConstrainLayout(Context context) { + this(context, null); + } + + public ShapeConstrainLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ShapeConstrainLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ShapeConstrainLayout); + int raduis = array.getDimensionPixelOffset(R.styleable.ShapeConstrainLayout_scl_radius, 0); + int soildColor = array.getColor(R.styleable.ShapeConstrainLayout_scl_solid, Color.WHITE); + int strokeWidth = array.getDimensionPixelOffset(R.styleable.ShapeConstrainLayout_scl_stroke_width, 0); + int strokeColor = array.getColor(R.styleable.ShapeConstrainLayout_scl_stroke_color, Color.WHITE); + + boolean hasSolid = array.hasValue(R.styleable.ShapeConstrainLayout_scl_solid); + if (hasSolid) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setColor(soildColor); + drawable.setCornerRadius(raduis); + if (strokeWidth > 0) { + try { + drawable.setStroke(strokeWidth, + strokeColor); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + setBackground(drawable); + } + //layout比例 + scaleWidth = array.getInteger(R.styleable.ShapeConstrainLayout_scl_scale_width, 0); + scaleHeight = array.getInteger(R.styleable.ShapeConstrainLayout_scl_scale_height, 0); + + array.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (scaleWidth > 0 && scaleHeight > 0) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int model = MeasureSpec.getMode(widthMeasureSpec); + int heiht = width * scaleHeight / scaleWidth; + heightMeasureSpec = MeasureSpec.makeMeasureSpec(heiht, model); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/library/src/main/java/com/chwl/library/widget/ViewItem.java b/library/src/main/java/com/chwl/library/widget/ViewItem.java new file mode 100644 index 0000000..40bd983 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/ViewItem.java @@ -0,0 +1,91 @@ +package com.chwl.library.widget; + +import com.chwl.library.R; + +public class ViewItem { + public static final int BUTTON_TYPE_NORMAL = 0; + public static final int BUTTON_TYPE_CANCEL = 1; + + + /** 发送礼物 */ + public static final int SEND_GIFT_ITEM = 0; + /** 锁坑 */ + public static final int SEND_LOCK_MIC_ITEM = 1; + /** 踢下麦 */ + public static final int SEND_KICKDOWN_MIC_ITEM = 2; + /** 踢出房间 */ + public static final int SEND_KICKOUT_ROOM_ITEM = 3; + /** 查看个人信息 */ + public static final int SEND_SHOW_USER_INCO_ITEM = 4; + /** 下麦 */ + public static final int SEND_DOWN_MIC_ITEM = 5; + /** 释放麦 */ + public static final int SEND_FREE_MIC_ITEM = 6; + /** 设置管理员 */ + public static final int SEND_MARK_MANAGER_ITEM = 7; + /** 取消管理员 */ + public static final int SEND_NOMARK_MANAGER_ITEM = 11; + /** 加入黑名单 */ + public static final int SEND_MARK_BLACK_ITEM = 8; + /** 开麦 */ + public static final int SEND_OPEN_MUTE_ITEM = 9; + /** 闭麦 */ + public static final int SEND_CLOSE_MUTE_ITEM = 10; + /** 抱上麦 */ + public static final int SEND_INVITE_MIC_ITEM = 12; + /** 发起竞拍 */ + public static final int START_AUCTION = 13; + + public String mText; + public int resourceID; + public int imgRes; + public OnClickListener mClickListener; + public int mButtonType; + public int mTheme = -1; + public boolean noDissmis; + + /**是否是关注按钮*/ + public boolean isAttent = false; + /**ture则的话,则显示在个人资料卡片底部*/ + public boolean isBottom = false; + /** + * 找到Ta + */ + public boolean isFindTa = false; + public ViewItem(String text, int imgRes, OnClickListener l) { + this(text, imgRes,BUTTON_TYPE_NORMAL, l); + } + public ViewItem(String text, int imgRes, boolean noDissmis, OnClickListener l) { + this(text, imgRes,BUTTON_TYPE_NORMAL, l); + this.noDissmis = noDissmis; + } + public ViewItem(String text, int imgRes, int buttonType, OnClickListener l) { + mText = text; + mClickListener = l; + mButtonType = buttonType; + this.imgRes = imgRes; + resourceID = R.layout.layout_user_card_item; + + } + + public ViewItem(String text, int imgRes, int buttonType, int theme, OnClickListener l) { + mText = text; + this.imgRes = imgRes; + mClickListener = l; + mButtonType = buttonType; + resourceID = R.layout.layout_user_card_item; + mTheme = theme; + } + + public void setText(String text) { + this.mText = text; + } + + public void setClickListener(OnClickListener mClickListener) { + this.mClickListener = mClickListener; + } + + public interface OnClickListener { + void onClick(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/decoration/RowColumnDifItemDecoration.java b/library/src/main/java/com/chwl/library/widget/decoration/RowColumnDifItemDecoration.java new file mode 100644 index 0000000..6388eba --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/decoration/RowColumnDifItemDecoration.java @@ -0,0 +1,64 @@ +package com.chwl.library.widget.decoration; + +import android.content.Context; +import android.graphics.Rect; +import android.util.TypedValue; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * + * 水平和竖直分割线宽度不同 + * 只适配垂直的布局 + * + */ +public class RowColumnDifItemDecoration extends RecyclerView.ItemDecoration { + + private int spanCount; + private int vPx; + private int hPx; + private boolean needTop; + private boolean needBottom; + + /** + * + * @param vDp 竖直分割线 + * @param hDp 水平分割线 + */ + public RowColumnDifItemDecoration(Context context, int spanCount, int vDp, int hDp, boolean needTop, boolean needBottom) { + this.spanCount = spanCount; + this.vPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + vDp, context.getResources().getDisplayMetrics()); + this.hPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + hDp, context.getResources().getDisplayMetrics()); + this.needTop = needTop; + this.needBottom = needBottom; + } + + @Override + public void getItemOffsets(Rect outRect, View child, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, child, parent, state); + + int pos = parent.getChildAdapterPosition(child); + int column = (pos) % spanCount;// 计算这个child 处于第几列 + + + //最后一行不添加 + int currRow = pos / spanCount; + int count = parent.getAdapter().getItemCount(); + int allRow = column / spanCount + (count % spanCount == 0 ? 0 : 1); + if (needTop && currRow == 0) { + outRect.top = hPx; + } else { + outRect.top = 0; + } + if (currRow < allRow - 1 || needBottom) { + outRect.bottom = hPx; + } else { + outRect.bottom = 0; + } + outRect.left = (column * vPx / spanCount); + outRect.right = vPx - (column + 1) * vPx / spanCount; + } +} diff --git a/library/src/main/java/com/chwl/library/widget/drag/ViewDragCallback.kt b/library/src/main/java/com/chwl/library/widget/drag/ViewDragCallback.kt new file mode 100644 index 0000000..ad7a3cd --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/drag/ViewDragCallback.kt @@ -0,0 +1,63 @@ +package com.chwl.library.widget.drag + +import android.view.View +import android.view.ViewGroup +import androidx.customview.widget.ViewDragHelper + +open class ViewDragCallback(private val parent: ViewGroup) : ViewDragHelper.Callback() { + + override fun tryCaptureView(child: View, pointerId: Int): Boolean { + return true + } + + override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { + if (isBoundaryLimit(child)) { + return (parent.height - child.height - parent.paddingBottom).coerceAtMost( + parent.paddingTop.coerceAtLeast( + top + ) + ) + } + return top + } + + override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int { + if (isBoundaryLimit(child)) { + return (parent.width - child.width - parent.paddingEnd).coerceAtMost( + parent.paddingStart.coerceAtLeast( + left + ) + ) + } + return left + } + + override fun onViewPositionChanged( + changedView: View, + left: Int, + top: Int, + dx: Int, + dy: Int + ) { + } + + override fun getOrderedChildIndex(index: Int): Int { + return index + } + + override fun getViewHorizontalDragRange(child: View): Int { + return parent.measuredWidth - child.measuredWidth + } + + override fun getViewVerticalDragRange(child: View): Int { + return parent.measuredHeight - child.measuredHeight + } + + /** + * 是否边界限制 + * @param child 要处理的view + */ + protected open fun isBoundaryLimit(child: View): Boolean { + return true + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/drag/ViewDragLayout.kt b/library/src/main/java/com/chwl/library/widget/drag/ViewDragLayout.kt new file mode 100644 index 0000000..46c9b44 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/drag/ViewDragLayout.kt @@ -0,0 +1,136 @@ +package com.chwl.library.widget.drag + +import android.content.Context +import android.graphics.Point +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.contains +import androidx.customview.widget.ViewDragHelper +import com.example.lib_utils.log.ILog + + +open class ViewDragLayout : FrameLayout, ILog { + + private var dragHelper: ViewDragHelper? = null + + private var syncPoint: Point? = null + + companion object { + /** + * 新创建一个同步点 + */ + fun createDragSyncPoint(): Point { + return Point(-1, -1) + } + } + + constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this( + context, + attrs, + defStyleAttr, + 0 + ) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + onInitialize() + } + + protected open fun onInitialize() { + initDrag(this) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + updateDragPosition() + } + + /** + * 初始化拖拽功能 + */ + protected fun initDrag(parent: ViewGroup) { + val dragCallback = object : ViewDragCallback(parent) { + + override fun onViewPositionChanged( + changedView: View, + left: Int, + top: Int, + dx: Int, + dy: Int + ) { + super.onViewPositionChanged(changedView, left, top, dx, dy) + getDragSyncPoint()?.set(left, top) + } + + override fun tryCaptureView(child: View, pointerId: Int): Boolean { + return this@ViewDragLayout.tryCaptureView(child, pointerId) + } + } + dragHelper = ViewDragHelper.create(parent, 1f, dragCallback) + } + + /** + * 更新同步之前的位置 + */ + open fun updateDragPosition() { + getDragSyncPoint()?.let { + if (it.x == -1 && it.y == -1) { + // 该同步点是初始状态 + return@let + } + val dragView = getDragView()?:return@let + dragView.offsetLeftAndRight(it.x - dragView.left) + dragView.offsetTopAndBottom(it.y - dragView.top) + } + } + + protected open fun getDragView(): View? { + return getChildAt(0) + } + + /** + * 可以拖拽? + */ + protected open fun tryCaptureView(child: View, pointerId: Int): Boolean{ + return child == getDragView() + } + + /** + * 拖拽位置同步记录 + */ + protected open fun getDragSyncPoint(): Point? { + if (syncPoint == null) { + syncPoint = createDragSyncPoint() + } + return syncPoint + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (ev?.action == MotionEvent.ACTION_DOWN) { + dragHelper?.findTopChildUnder(ev.x.toInt(), ev.y.toInt()) ?: return false + } + return super.dispatchTouchEvent(ev) + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + return dragHelper?.shouldInterceptTouchEvent(event) ?: super.onInterceptTouchEvent(event) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + return if (dragHelper == null) { + super.onTouchEvent(event) + } else { + dragHelper?.processTouchEvent(event) + true + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/tab/BaseTabTitleProvider.kt b/library/src/main/java/com/chwl/library/widget/tab/BaseTabTitleProvider.kt new file mode 100644 index 0000000..ed497c7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/BaseTabTitleProvider.kt @@ -0,0 +1,7 @@ +package com.chwl.library.widget.tab + +class BaseTabTitleProvider(var mTitleList:List?) : TabTitleProvider { + override fun getPageTitle(position: Int): CharSequence? { + return mTitleList?.get(position)?:"" + } +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/tab/SmartTabIndicationInterpolator.java b/library/src/main/java/com/chwl/library/widget/tab/SmartTabIndicationInterpolator.java new file mode 100644 index 0000000..2ae1776 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/SmartTabIndicationInterpolator.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +public abstract class SmartTabIndicationInterpolator { + + public static final SmartTabIndicationInterpolator SMART = new SmartIndicationInterpolator(); + public static final SmartTabIndicationInterpolator LINEAR = new LinearIndicationInterpolator(); + + static final int ID_SMART = 0; + static final int ID_LINEAR = 1; + + public static SmartTabIndicationInterpolator of(int id) { + switch (id) { + case ID_SMART: + return SMART; + case ID_LINEAR: + return LINEAR; + default: + throw new IllegalArgumentException("Unknown id: " + id); + } + } + + public abstract float getLeftEdge(float offset); + + public abstract float getRightEdge(float offset); + + public float getThickness(float offset) { + return 1f; //Always the same thickness by default + } + + public static class SmartIndicationInterpolator extends SmartTabIndicationInterpolator { + + private static final float DEFAULT_INDICATOR_INTERPOLATION_FACTOR = 3.0f; + + private final Interpolator leftEdgeInterpolator; + private final Interpolator rightEdgeInterpolator; + + public SmartIndicationInterpolator() { + this(DEFAULT_INDICATOR_INTERPOLATION_FACTOR); + } + + public SmartIndicationInterpolator(float factor) { + leftEdgeInterpolator = new AccelerateInterpolator(factor); + rightEdgeInterpolator = new DecelerateInterpolator(factor); + } + + @Override + public float getLeftEdge(float offset) { + return leftEdgeInterpolator.getInterpolation(offset); + } + + @Override + public float getRightEdge(float offset) { + return rightEdgeInterpolator.getInterpolation(offset); + } + + @Override + public float getThickness(float offset) { + return 1f / (1.0f - getLeftEdge(offset) + getRightEdge(offset)); + } + + } + + public static class LinearIndicationInterpolator extends SmartTabIndicationInterpolator { + + @Override + public float getLeftEdge(float offset) { + return offset; + } + + @Override + public float getRightEdge(float offset) { + return offset; + } + + } +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout.java b/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout.java new file mode 100644 index 0000000..de93a2e --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout.java @@ -0,0 +1,681 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.os.Build; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.view.ViewCompat; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.chwl.library.R; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as + * to + * the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link androidx.fragment.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this + * layout + * is being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to provide an array of + * colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + *

+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + *

+ * Forked from Google Samples > SlidingTabsBasic > + * SlidingTabLayout + */ +public class SmartTabLayout extends HorizontalScrollView { + + private static final boolean DEFAULT_DISTRIBUTE_EVENLY = false; + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TITLE_OFFSET_AUTO_CENTER = -1; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final boolean TAB_VIEW_TEXT_ALL_CAPS = true; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + private static final int TAB_VIEW_TEXT_COLOR = 0xFC000000; + private static final int TAB_VIEW_TEXT_MIN_WIDTH = 0; + private static final boolean TAB_CLICKABLE = true; + private static final int NO_TEXT_STYLE = -1; + + protected final SmartTabStrip tabStrip; + private int titleOffset; + private int tabViewBackgroundResId; + private boolean tabViewTextAllCaps; + private ColorStateList tabViewTextColors; + private float tabViewTextSize; + private int tabViewTextHorizontalPadding; + private int tabViewTextMinWidth; + private ViewPager viewPager; + private ViewPager.OnPageChangeListener viewPagerPageChangeListener; + private OnScrollChangeListener onScrollChangeListener; + private TabProvider tabProvider; + private InternalTabClickListener internalTabClickListener; + private OnTabClickListener onTabClickListener; + private boolean distributeEvenly; + private int textAppearance; + + public SmartTabLayout(Context context) { + this(context, null); + } + + public SmartTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SmartTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + + final DisplayMetrics dm = getResources().getDisplayMetrics(); + final float density = dm.density; + + int tabBackgroundResId = NO_ID; + boolean textAllCaps = TAB_VIEW_TEXT_ALL_CAPS; + ColorStateList textColors; + float textSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP, dm); + int textHorizontalPadding = (int) (TAB_VIEW_PADDING_DIPS * density); + int textMinWidth = (int) (TAB_VIEW_TEXT_MIN_WIDTH * density); + boolean distributeEvenly = DEFAULT_DISTRIBUTE_EVENLY; + int customTabLayoutId = NO_ID; + int customTabTextViewId = NO_ID; + boolean clickable = TAB_CLICKABLE; + int titleOffset = (int) (TITLE_OFFSET_DIPS * density); + int textStyle = NO_TEXT_STYLE; + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SmartTabLayout, defStyle, 0); + tabBackgroundResId = a.getResourceId( + R.styleable.SmartTabLayout_stl_defaultTabBackground, tabBackgroundResId); + textAllCaps = a.getBoolean( + R.styleable.SmartTabLayout_stl_defaultTabTextAllCaps, textAllCaps); + textColors = a.getColorStateList( + R.styleable.SmartTabLayout_stl_defaultTabTextColor); + textSize = a.getDimension( + R.styleable.SmartTabLayout_stl_defaultTabTextSize, textSize); + textHorizontalPadding = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_defaultTabTextHorizontalPadding, textHorizontalPadding); + textMinWidth = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_defaultTabTextMinWidth, textMinWidth); + customTabLayoutId = a.getResourceId( + R.styleable.SmartTabLayout_stl_customTabTextLayoutId, customTabLayoutId); + customTabTextViewId = a.getResourceId( + R.styleable.SmartTabLayout_stl_customTabTextViewId, customTabTextViewId); + distributeEvenly = a.getBoolean( + R.styleable.SmartTabLayout_stl_distributeEvenly, distributeEvenly); + clickable = a.getBoolean( + R.styleable.SmartTabLayout_stl_clickable, clickable); + titleOffset = a.getLayoutDimension( + R.styleable.SmartTabLayout_stl_titleOffset, titleOffset); + textStyle = a.getResourceId(R.styleable.SmartTabLayout_stl_tabTextStyle, textStyle); + a.recycle(); + + this.titleOffset = titleOffset; + this.tabViewBackgroundResId = tabBackgroundResId; + this.tabViewTextAllCaps = textAllCaps; + this.tabViewTextColors = (textColors != null) + ? textColors + : ColorStateList.valueOf(TAB_VIEW_TEXT_COLOR); + this.tabViewTextSize = textSize; + this.tabViewTextHorizontalPadding = textHorizontalPadding; + this.tabViewTextMinWidth = textMinWidth; + this.internalTabClickListener = clickable ? new InternalTabClickListener() : null; + this.distributeEvenly = distributeEvenly; + this.textAppearance = textStyle; + + if (customTabLayoutId != NO_ID) { + setCustomTabView(customTabLayoutId, customTabTextViewId); + } + + this.tabStrip = new SmartTabStrip(context, attrs); + + if (distributeEvenly && tabStrip.isIndicatorAlwaysInCenter()) { + throw new UnsupportedOperationException( + "'distributeEvenly' and 'indicatorAlwaysInCenter' both use does not support"); + } + + // Make sure that the Tab Strips fills this View + setFillViewport(!tabStrip.isIndicatorAlwaysInCenter()); + + addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + if (onScrollChangeListener != null) { + onScrollChangeListener.onScrollChanged(l, oldl); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (tabStrip.isIndicatorAlwaysInCenter() && tabStrip.getChildCount() > 0) { + View firstTab = tabStrip.getChildAt(0); + View lastTab = tabStrip.getChildAt(tabStrip.getChildCount() - 1); + int start = (w - Utils.getMeasuredWidth(firstTab)) / 2 - Utils.getMarginStart(firstTab); + int end = (w - Utils.getMeasuredWidth(lastTab)) / 2 - Utils.getMarginEnd(lastTab); + tabStrip.setMinimumWidth(tabStrip.getMeasuredWidth()); + ViewCompat.setPaddingRelative(this, start, getPaddingTop(), end, getPaddingBottom()); + setClipToPadding(false); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + // Ensure first scroll + if (changed && viewPager != null) { + scrollToTab(viewPager.getCurrentItem(), 0); + } + } + + /** + * Set the behavior of the Indicator scrolling feedback. + * + * @param interpolator {@link com.ogaclejapan.smarttablayout.SmartTabIndicationInterpolator} + */ + public void setIndicationInterpolator(SmartTabIndicationInterpolator interpolator) { + tabStrip.setIndicationInterpolator(interpolator); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple customisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + tabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Set the color used for styling the tab text. This will need to be called prior to calling + * {@link #setViewPager(ViewPager)} otherwise it will not get set + * + * @param color to use for tab text + */ + public void setDefaultTabTextColor(int color) { + tabViewTextColors = ColorStateList.valueOf(color); + } + + /** + * Sets the colors used for styling the tab text. This will need to be called prior to calling + * {@link #setViewPager(ViewPager)} otherwise it will not get set + * + * @param colors ColorStateList to use for tab text + */ + public void setDefaultTabTextColor(ColorStateList colors) { + tabViewTextColors = colors; + } + + /** + * Set the same weight for tab + */ + public void setDistributeEvenly(boolean distributeEvenly) { + this.distributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + tabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + tabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SmartTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + viewPagerPageChangeListener = listener; + } + + /** + * Set {@link OnScrollChangeListener} for obtaining values of scrolling. + * + * @param listener the {@link OnScrollChangeListener} to set + */ + public void setOnScrollChangeListener(OnScrollChangeListener listener) { + onScrollChangeListener = listener; + } + + /** + * Set {@link OnTabClickListener} for obtaining click event. + * + * @param listener the {@link OnTabClickListener} to set + */ + public void setOnTabClickListener(OnTabClickListener listener) { + onTabClickListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + tabProvider = new SimpleTabProvider(getContext(), layoutResId, textViewId); + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param provider {@link TabProvider} + */ + public void setCustomTabView(TabProvider provider) { + tabProvider = provider; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + tabStrip.removeAllViews(); + + this.viewPager = viewPager; + if (viewPager != null && viewPager.getAdapter() != null) { + viewPager.addOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + public void setViews(int count,TabProvider provider) { + setCustomTabView(provider); + tabStrip.removeAllViews(); + populateTabViews(count); + } + + /** + * Returns the view at the specified position in the tabs. + * + * @param position the position at which to get the view from + * @return the view at the specified position or null if the position does not exist within the + * tabs + */ + public View getTabAt(int position) { + return tabStrip.getChildAt(position); + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(CharSequence title) { + TextView textView = new TextView(getContext()); + textView.setGravity(Gravity.CENTER); + textView.setText(title); + if (textAppearance != NO_TEXT_STYLE) { + textView.setTextAppearance(getContext(), textAppearance); + } else { + textView.setTextColor(tabViewTextColors); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabViewTextSize); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(tabViewTextAllCaps); + } + + if (tabViewTextMinWidth > 0) { + textView.setMinWidth(tabViewTextMinWidth); + } + } + if (tabViewBackgroundResId != NO_ID) { + textView.setBackgroundResource(tabViewBackgroundResId); + } else { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + textView.setPadding( + tabViewTextHorizontalPadding, 0, + tabViewTextHorizontalPadding, 0); + textView.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = viewPager.getAdapter(); + + for (int i = 0; i < adapter.getCount(); i++) { + + final View tabView = (tabProvider == null) + ? createDefaultTabView(adapter.getPageTitle(i)) + : tabProvider.createTabView(tabStrip, i, adapter); + + if (tabView == null) { + throw new IllegalStateException("tabView is null."); + } + + if (distributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + + if (internalTabClickListener != null) { + tabView.setOnClickListener(internalTabClickListener); + } + + tabStrip.addView(tabView); + + if (i == viewPager.getCurrentItem()) { + tabView.setSelected(true); + } + + } + } + + private void populateTabViews(int count) { + + try { + for (int i = 0; i < count; i++) { + + final View tabView = tabProvider.createTabView(tabStrip, i, null); + + if (tabView == null) { + throw new IllegalStateException("tabView is null."); + } + + if (distributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + + if (internalTabClickListener != null) { + tabView.setOnClickListener(internalTabClickListener); + } + + tabStrip.addView(tabView); + } + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + } + + public void scrollToTab(int tabIndex, float positionOffset) { + final int tabStripChildCount = tabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + View selectedTab = tabStrip.getChildAt(tabIndex); + int widthPlusMargin = Utils.getWidth(selectedTab) + Utils.getMarginHorizontally(selectedTab); + int extraOffset = (int) (positionOffset * widthPlusMargin); + + if (tabStrip.isIndicatorAlwaysInCenter()) { + + if (0f < positionOffset && positionOffset < 1f) { + View nextTab = tabStrip.getChildAt(tabIndex + 1); + int selectHalfWidth = Utils.getWidth(selectedTab) / 2 + Utils.getMarginEnd(selectedTab); + int nextHalfWidth = Utils.getWidth(nextTab) / 2 + Utils.getMarginStart(nextTab); + extraOffset = Math.round(positionOffset * (selectHalfWidth + nextHalfWidth)); + } + + View firstTab = tabStrip.getChildAt(0); + int x; + if (isLayoutRtl) { + int first = Utils.getWidth(firstTab) + Utils.getMarginEnd(firstTab); + int selected = Utils.getWidth(selectedTab) + Utils.getMarginEnd(selectedTab); + x = Utils.getEnd(selectedTab) - Utils.getMarginEnd(selectedTab) - extraOffset; + x -= (first - selected) / 2; + } else { + int first = Utils.getWidth(firstTab) + Utils.getMarginStart(firstTab); + int selected = Utils.getWidth(selectedTab) + Utils.getMarginStart(selectedTab); + x = Utils.getStart(selectedTab) - Utils.getMarginStart(selectedTab) + extraOffset; + x -= (first - selected) / 2; + } + + scrollTo(x, 0); + return; + + } + + int x; + if (titleOffset == TITLE_OFFSET_AUTO_CENTER) { + + if (0f < positionOffset && positionOffset < 1f) { + View nextTab = tabStrip.getChildAt(tabIndex + 1); + int selectHalfWidth = Utils.getWidth(selectedTab) / 2 + Utils.getMarginEnd(selectedTab); + int nextHalfWidth = Utils.getWidth(nextTab) / 2 + Utils.getMarginStart(nextTab); + extraOffset = Math.round(positionOffset * (selectHalfWidth + nextHalfWidth)); + } + + if (isLayoutRtl) { + x = -Utils.getWidthWithMargin(selectedTab) / 2 + getWidth() / 2; + x -= Utils.getPaddingStart(this); + } else { + x = Utils.getWidthWithMargin(selectedTab) / 2 - getWidth() / 2; + x += Utils.getPaddingStart(this); + } + + } else { + + if (isLayoutRtl) { + x = (tabIndex > 0 || positionOffset > 0) ? titleOffset : 0; + } else { + x = (tabIndex > 0 || positionOffset > 0) ? -titleOffset : 0; + } + + } + + int start = Utils.getStart(selectedTab); + int startMargin = Utils.getMarginStart(selectedTab); + if (isLayoutRtl) { + x += start + startMargin - extraOffset - getWidth() + Utils.getPaddingHorizontally(this); + } else { + x += start - startMargin + extraOffset; + } + + scrollTo(x, 0); + + } + + /** + * Interface definition for a callback to be invoked when the scroll position of a view changes. + */ + public interface OnScrollChangeListener { + + /** + * Called when the scroll position of a view changes. + * + * @param scrollX Current horizontal scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + */ + void onScrollChanged(int scrollX, int oldScrollX); + } + + /** + * Interface definition for a callback to be invoked when a tab is clicked. + */ + public interface OnTabClickListener { + + /** + * Called when a tab is clicked. + * + * @param position tab's position + */ + void onTabClicked(int position,View view); + } + + /** + * Create the custom tabs in the tab layout. Set with + * {@link #setCustomTabView(com.ogaclejapan.smarttablayout.SmartTabLayout.TabProvider)} + */ + public interface TabProvider { + + /** + * @return Return the View of {@code position} for the Tabs + */ + View createTabView(ViewGroup container, int position, PagerAdapter adapter); + + } + + private static class SimpleTabProvider implements TabProvider { + + private final LayoutInflater inflater; + private final int tabViewLayoutId; + private final int tabViewTextViewId; + + private SimpleTabProvider(Context context, int layoutResId, int textViewId) { + inflater = LayoutInflater.from(context); + tabViewLayoutId = layoutResId; + tabViewTextViewId = textViewId; + } + + @Override + public View createTabView(ViewGroup container, int position, PagerAdapter adapter) { + View tabView = null; + TextView tabTitleView = null; + + if (tabViewLayoutId != NO_ID) { + tabView = inflater.inflate(tabViewLayoutId, container, false); + } + + if (tabViewTextViewId != NO_ID && tabView != null) { + tabTitleView = (TextView) tabView.findViewById(tabViewTextViewId); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + if (tabTitleView != null) { + tabTitleView.setText(adapter.getPageTitle(position)); + } + + return tabView; + } + + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + + private int scrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = tabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + tabStrip.onViewPagerPageChanged(position, positionOffset); + + scrollToTab(position, positionOffset); + + if (viewPagerPageChangeListener != null) { + viewPagerPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + scrollState = state; + + if (viewPagerPageChangeListener != null) { + viewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (scrollState == ViewPager.SCROLL_STATE_IDLE) { + tabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) { + tabStrip.getChildAt(i).setSelected(position == i); + } + + if (viewPagerPageChangeListener != null) { + viewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class InternalTabClickListener implements OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < tabStrip.getChildCount(); i++) { + if (v == tabStrip.getChildAt(i)) { + if (onTabClickListener != null) { + onTabClickListener.onTabClicked(i,v); + } + if (viewPager != null) { + viewPager.setCurrentItem(i); + } + return; + } + } + } + } + + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout2.java b/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout2.java new file mode 100644 index 0000000..2787ac5 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/SmartTabLayout2.java @@ -0,0 +1,709 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.os.Build; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; + +import com.chwl.library.R; + +/** + * To be used with ViewPager2 to provide a tab indicator component which give constant feedback as + * to + * the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link androidx.fragment.app.Fragment} call + * {@link #setViewPager(ViewPager2, RecyclerView.Adapter, TabTitleProvider)} providing it the ViewPager this + * layout + * is being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to provide an array of + * colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + *

+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + *

+ * Forked from Google Samples > SlidingTabsBasic > + * SlidingTabLayout + */ +public class SmartTabLayout2 extends HorizontalScrollView { + + private static final boolean DEFAULT_DISTRIBUTE_EVENLY = false; + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TITLE_OFFSET_AUTO_CENTER = -1; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final boolean TAB_VIEW_TEXT_ALL_CAPS = true; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + private static final int TAB_VIEW_TEXT_COLOR = 0xFC000000; + private static final int TAB_VIEW_TEXT_MIN_WIDTH = 0; + private static final boolean TAB_CLICKABLE = true; + private static final int NO_TEXT_STYLE = -1; + + protected final SmartTabStrip2 tabStrip; + private int titleOffset; + private int tabViewBackgroundResId; + private boolean tabViewTextAllCaps; + private ColorStateList tabViewTextColors; + private float tabViewTextSize; + private int tabViewTextHorizontalPadding; + private int tabViewTextMinWidth; + private ViewPager2 viewPager; + private ViewPager2.OnPageChangeCallback viewPagerPageChangeCallback; + @Nullable + private TabTitleProvider tabTitleProvider; + private OnScrollChangeListener onScrollChangeListener; + private TabProvider tabProvider; + private InternalTabClickListener internalTabClickListener; + private InternalTabLongClickListener internalTabLongClickListener; + private OnTabClickListener onTabClickListener; + private OnTabLongClickListener onTabLongClickListener; + private boolean distributeEvenly; + private int textAppearance; + + private int childCountCache = 0; + + public SmartTabLayout2(Context context) { + this(context, null); + } + + public SmartTabLayout2(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SmartTabLayout2(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + + final DisplayMetrics dm = getResources().getDisplayMetrics(); + final float density = dm.density; + + int tabBackgroundResId = NO_ID; + boolean textAllCaps = TAB_VIEW_TEXT_ALL_CAPS; + ColorStateList textColors; + float textSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP, dm); + int textHorizontalPadding = (int) (TAB_VIEW_PADDING_DIPS * density); + int textMinWidth = (int) (TAB_VIEW_TEXT_MIN_WIDTH * density); + boolean distributeEvenly = DEFAULT_DISTRIBUTE_EVENLY; + int customTabLayoutId = NO_ID; + int customTabTextViewId = NO_ID; + boolean clickable = TAB_CLICKABLE; + int titleOffset = (int) (TITLE_OFFSET_DIPS * density); + int textStyle = NO_TEXT_STYLE; + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SmartTabLayout2, defStyle, 0); + tabBackgroundResId = a.getResourceId( + R.styleable.SmartTabLayout2_stl_defaultTabBackground, tabBackgroundResId); + textAllCaps = a.getBoolean( + R.styleable.SmartTabLayout2_stl_defaultTabTextAllCaps, textAllCaps); + textColors = a.getColorStateList( + R.styleable.SmartTabLayout2_stl_defaultTabTextColor); + textSize = a.getDimension( + R.styleable.SmartTabLayout2_stl_defaultTabTextSize, textSize); + textHorizontalPadding = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_defaultTabTextHorizontalPadding, textHorizontalPadding); + textMinWidth = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_defaultTabTextMinWidth, textMinWidth); + customTabLayoutId = a.getResourceId( + R.styleable.SmartTabLayout2_stl_customTabTextLayoutId, customTabLayoutId); + customTabTextViewId = a.getResourceId( + R.styleable.SmartTabLayout2_stl_customTabTextViewId, customTabTextViewId); + distributeEvenly = a.getBoolean( + R.styleable.SmartTabLayout2_stl_distributeEvenly, distributeEvenly); + clickable = a.getBoolean( + R.styleable.SmartTabLayout2_stl_clickable, clickable); + titleOffset = a.getLayoutDimension( + R.styleable.SmartTabLayout2_stl_titleOffset, titleOffset); + textStyle = a.getResourceId(R.styleable.SmartTabLayout2_stl_tabTextStyle, textStyle); + a.recycle(); + + this.titleOffset = titleOffset; + this.tabViewBackgroundResId = tabBackgroundResId; + this.tabViewTextAllCaps = textAllCaps; + this.tabViewTextColors = (textColors != null) + ? textColors + : ColorStateList.valueOf(TAB_VIEW_TEXT_COLOR); + this.tabViewTextSize = textSize; + this.tabViewTextHorizontalPadding = textHorizontalPadding; + this.tabViewTextMinWidth = textMinWidth; + this.internalTabClickListener = clickable ? new InternalTabClickListener() : null; + this.internalTabLongClickListener = clickable ? new InternalTabLongClickListener() : null; + this.distributeEvenly = distributeEvenly; + this.textAppearance = textStyle; + + if (customTabLayoutId != NO_ID) { + setCustomTabView(customTabLayoutId, customTabTextViewId); + } + + this.tabStrip = new SmartTabStrip2(context, attrs); + + if (distributeEvenly && tabStrip.isIndicatorAlwaysInCenter()) { + throw new UnsupportedOperationException( + "'distributeEvenly' and 'indicatorAlwaysInCenter' both use does not support"); + } + + // Make sure that the Tab Strips fills this View + setFillViewport(!tabStrip.isIndicatorAlwaysInCenter()); + + addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + if (onScrollChangeListener != null) { + onScrollChangeListener.onScrollChanged(l, oldl); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + adjustForIndicatorAlwaysInCenter(w, h); + } + + private void adjustForIndicatorAlwaysInCenter(int w, int h) { + if (tabStrip.isIndicatorAlwaysInCenter() && tabStrip.getChildCount() > 0) { + View firstTab = tabStrip.getChildAt(0); + View lastTab = tabStrip.getChildAt(tabStrip.getChildCount() - 1); + int start = (w - Utils.getMeasuredWidth(firstTab)) / 2 - Utils.getMarginStart(firstTab); + int end = (w - Utils.getMeasuredWidth(lastTab)) / 2 - Utils.getMarginEnd(lastTab); + tabStrip.setMinimumWidth(tabStrip.getMeasuredWidth()); + ViewCompat.setPaddingRelative(this, start, getPaddingTop(), end, getPaddingBottom()); + setClipToPadding(false); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + // Ensure first scroll + if (changed && viewPager != null) { + scrollToTab(viewPager.getCurrentItem(), 0); + } + if (childCountCache != tabStrip.getChildCount()) { + adjustForIndicatorAlwaysInCenter(getMeasuredWidth(), getMeasuredHeight()); + } + childCountCache = tabStrip.getChildCount(); + } + + /** + * Set the behavior of the Indicator scrolling feedback. + * + * @param interpolator {@link SmartTabIndicationInterpolator} + */ + public void setIndicationInterpolator(SmartTabIndicationInterpolator interpolator) { + tabStrip.setIndicationInterpolator(interpolator); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple customisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + tabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Set the color used for styling the tab text. This will need to be called prior to calling + * {@link #setViewPager(ViewPager2, RecyclerView.Adapter, TabTitleProvider)} otherwise it will not get set + * + * @param color to use for tab text + */ + public void setDefaultTabTextColor(int color) { + tabViewTextColors = ColorStateList.valueOf(color); + } + + /** + * Sets the colors used for styling the tab text. This will need to be called prior to calling + * {@link #setViewPager(ViewPager2, RecyclerView.Adapter, TabTitleProvider)} otherwise it will not get set + * + * @param colors ColorStateList to use for tab text + */ + public void setDefaultTabTextColor(ColorStateList colors) { + tabViewTextColors = colors; + } + + /** + * Set the same weight for tab + */ + public void setDistributeEvenly(boolean distributeEvenly) { + this.distributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + tabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + tabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager2.OnPageChangeCallback}. When using {@link SmartTabLayout2} you are + * required to set any {@link ViewPager2.OnPageChangeCallback} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager2#registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback) + */ + public void setOnPageChangeCallback(ViewPager2.OnPageChangeCallback listener) { + viewPagerPageChangeCallback = listener; + } + + /** + * Set {@link OnScrollChangeListener} for obtaining values of scrolling. + * + * @param listener the {@link OnScrollChangeListener} to set + */ + public void setOnScrollChangeListener(OnScrollChangeListener listener) { + onScrollChangeListener = listener; + } + + /** + * Set {@link OnTabClickListener} for obtaining click event. + * + * @param listener the {@link OnTabClickListener} to set + */ + public void setOnTabClickListener(OnTabClickListener listener) { + onTabClickListener = listener; + } + + public void setOnTabLongClickListener(OnTabLongClickListener listener) { + onTabLongClickListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + tabProvider = new SimpleTabProvider(getContext(), layoutResId, textViewId); + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param provider {@link TabProvider} + */ + public void setCustomTabView(TabProvider provider) { + tabProvider = provider; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(@NonNull ViewPager2 viewPager, @NonNull RecyclerView.Adapter adapter, + @Nullable TabTitleProvider titleProvider) { + tabStrip.removeAllViews(); + + this.viewPager = viewPager; + if (this.viewPager.getAdapter() != adapter){ + this.viewPager.setAdapter(adapter); + } + this.tabTitleProvider = titleProvider; + if (viewPager != null && viewPager.getAdapter() != null) { + viewPager.registerOnPageChangeCallback(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Returns the view at the specified position in the tabs. + * + * @param position the position at which to get the view from + * @return the view at the specified position or null if the position does not exist within the + * tabs + */ + public View getTabAt(int position) { + return tabStrip.getChildAt(position); + } + + public int getTabCount() { + return tabStrip.getChildCount(); + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(CharSequence title) { + TextView textView = new TextView(getContext()); + textView.setGravity(Gravity.CENTER); + textView.setText(title); + if (textAppearance != NO_TEXT_STYLE) { + textView.setTextAppearance(getContext(), textAppearance); + } else { + textView.setTextColor(tabViewTextColors); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabViewTextSize); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(tabViewTextAllCaps); + } + + if (tabViewTextMinWidth > 0) { + textView.setMinWidth(tabViewTextMinWidth); + } + } + if (tabViewBackgroundResId != NO_ID) { + textView.setBackgroundResource(tabViewBackgroundResId); + } else { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + textView.setPadding( + tabViewTextHorizontalPadding, 0, + tabViewTextHorizontalPadding, 0); + textView.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); + + return textView; + } + + private void populateTabStrip() { + final RecyclerView.Adapter adapter = viewPager.getAdapter(); + + for (int i = 0; i < adapter.getItemCount(); i++) { + + CharSequence pageTitle = null; + if (tabTitleProvider != null) { + pageTitle = tabTitleProvider.getPageTitle(i); + } + + final View tabView = (tabProvider == null) + ? createDefaultTabView(pageTitle) + : tabProvider.createTabView(tabStrip, i, pageTitle); + + if (tabView == null) { + throw new IllegalStateException("tabView is null."); + } + + if (distributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + + if (internalTabClickListener != null) { + tabView.setOnClickListener(internalTabClickListener); + } + if (internalTabLongClickListener != null) { + tabView.setOnLongClickListener(internalTabLongClickListener); + } + + tabStrip.addView(tabView); + + if (i == viewPager.getCurrentItem()) { + tabView.setSelected(true); + } + + } + } + + private void scrollToTab(int tabIndex, float positionOffset) { + final int tabStripChildCount = tabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + View selectedTab = tabStrip.getChildAt(tabIndex); + int widthPlusMargin = Utils.getWidth(selectedTab) + Utils.getMarginHorizontally(selectedTab); + int extraOffset = (int) (positionOffset * widthPlusMargin); + + if (tabStrip.isIndicatorAlwaysInCenter()) { + + if (0f < positionOffset && positionOffset < 1f) { + View nextTab = tabStrip.getChildAt(tabIndex + 1); + int selectHalfWidth = Utils.getWidth(selectedTab) / 2 + Utils.getMarginEnd(selectedTab); + int nextHalfWidth = Utils.getWidth(nextTab) / 2 + Utils.getMarginStart(nextTab); + extraOffset = Math.round(positionOffset * (selectHalfWidth + nextHalfWidth)); + } + + View firstTab = tabStrip.getChildAt(0); + int x; + if (isLayoutRtl) { + int first = Utils.getWidth(firstTab) + Utils.getMarginEnd(firstTab); + int selected = Utils.getWidth(selectedTab) + Utils.getMarginEnd(selectedTab); + x = Utils.getEnd(selectedTab) - Utils.getMarginEnd(selectedTab) - extraOffset; + x -= (first - selected) / 2; + } else { + int first = Utils.getWidth(firstTab) + Utils.getMarginStart(firstTab); + int selected = Utils.getWidth(selectedTab) + Utils.getMarginStart(selectedTab); + x = Utils.getStart(selectedTab) - Utils.getMarginStart(selectedTab) + extraOffset; + x -= (first - selected) / 2; + } + + scrollTo(x, 0); + return; + + } + + int x; + if (titleOffset == TITLE_OFFSET_AUTO_CENTER) { + + if (0f < positionOffset && positionOffset < 1f) { + View nextTab = tabStrip.getChildAt(tabIndex + 1); + int selectHalfWidth = Utils.getWidth(selectedTab) / 2 + Utils.getMarginEnd(selectedTab); + int nextHalfWidth = Utils.getWidth(nextTab) / 2 + Utils.getMarginStart(nextTab); + extraOffset = Math.round(positionOffset * (selectHalfWidth + nextHalfWidth)); + } + + if (isLayoutRtl) { + x = -Utils.getWidthWithMargin(selectedTab) / 2 + getWidth() / 2; + x -= Utils.getPaddingStart(this); + } else { + x = Utils.getWidthWithMargin(selectedTab) / 2 - getWidth() / 2; + x += Utils.getPaddingStart(this); + } + + } else { + + if (isLayoutRtl) { + x = (tabIndex > 0 || positionOffset > 0) ? titleOffset : 0; + } else { + x = (tabIndex > 0 || positionOffset > 0) ? -titleOffset : 0; + } + + } + + int start = Utils.getStart(selectedTab); + int startMargin = Utils.getMarginStart(selectedTab); + if (isLayoutRtl) { + x += start + startMargin - extraOffset - getWidth() + Utils.getPaddingHorizontally(this); + } else { + x += start - startMargin + extraOffset; + } + + scrollTo(x, 0); + + } + + /** + * Interface definition for a callback to be invoked when the scroll position of a view changes. + */ + public interface OnScrollChangeListener { + + /** + * Called when the scroll position of a view changes. + * + * @param scrollX Current horizontal scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + */ + void onScrollChanged(int scrollX, int oldScrollX); + } + + /** + * Interface definition for a callback to be invoked when a tab is clicked. + */ + public interface OnTabClickListener { + + /** + * Called when a tab is clicked. + * + * @param position tab's position + */ + void onTabClicked(int position); + } + + /** + * Interface definition for a callback to be invoked when a tab is long clicked. + */ + public interface OnTabLongClickListener { + + /** + * Called when a tab is clicked. + * + * @param position tab's position + */ + void onTabLongClicked(int position); + } + + /** + * Create the custom tabs in the tab layout. Set with + * {@link #setCustomTabView(TabProvider)} + */ + public interface TabProvider { + + /** + * @return Return the View of {@code position} for the Tabs + */ + View createTabView(ViewGroup container, int position, @Nullable CharSequence pageTitle); + + } + + private static class SimpleTabProvider implements TabProvider { + + private final LayoutInflater inflater; + private final int tabViewLayoutId; + private final int tabViewTextViewId; + + private SimpleTabProvider(Context context, int layoutResId, int textViewId) { + inflater = LayoutInflater.from(context); + tabViewLayoutId = layoutResId; + tabViewTextViewId = textViewId; + } + + @Override + public View createTabView(ViewGroup container, int position, CharSequence pageTitle) { + View tabView = null; + TextView tabTitleView = null; + + if (tabViewLayoutId != NO_ID) { + tabView = inflater.inflate(tabViewLayoutId, container, false); + } + + if (tabViewTextViewId != NO_ID && tabView != null) { + tabTitleView = (TextView) tabView.findViewById(tabViewTextViewId); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + if (tabTitleView != null) { + tabTitleView.setText(pageTitle); + } + + return tabView; + } + + } + + private class InternalViewPagerListener extends ViewPager2.OnPageChangeCallback { + + private int scrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = tabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + tabStrip.onViewPagerPageChanged(position, positionOffset); + + scrollToTab(position, positionOffset); + + if (viewPagerPageChangeCallback != null) { + viewPagerPageChangeCallback.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + scrollState = state; + + if (viewPagerPageChangeCallback != null) { + viewPagerPageChangeCallback.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (scrollState == ViewPager2.SCROLL_STATE_IDLE) { + tabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) { + tabStrip.getChildAt(i).setSelected(position == i); + } + + if (viewPagerPageChangeCallback != null) { + viewPagerPageChangeCallback.onPageSelected(position); + } + } + + } + + private class InternalTabClickListener implements OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < tabStrip.getChildCount(); i++) { + if (v == tabStrip.getChildAt(i)) { + if (onTabClickListener != null) { + onTabClickListener.onTabClicked(i); + } + viewPager.setCurrentItem(i); + return; + } + } + } + } + + private class InternalTabLongClickListener implements OnLongClickListener { + @Override + public boolean onLongClick(View v) { + for (int i = 0; i < tabStrip.getChildCount(); i++) { + if (v == tabStrip.getChildAt(i)) { + if (onTabLongClickListener != null) { + onTabLongClickListener.onTabLongClicked(i); + } + return true; + } + } + return false; + } + } +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip.java b/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip.java new file mode 100644 index 0000000..05be057 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip.java @@ -0,0 +1,446 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +import com.chwl.library.R; + +/** + *

+ * Forked from Google Samples > SlidingTabsBasic > + * SlidingTabStrip + */ +class SmartTabStrip extends LinearLayout { + + private static final int GRAVITY_BOTTOM = 0; + private static final int GRAVITY_TOP = 1; + private static final int GRAVITY_CENTER = 2; + + private static final int AUTO_WIDTH = -1; + + private static final int DEFAULT_TOP_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_TOP_BORDER_COLOR_ALPHA = 0x26; + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + private static final float DEFAULT_INDICATOR_CORNER_RADIUS = 0f; + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + private static final boolean DEFAULT_INDICATOR_IN_CENTER = false; + private static final boolean DEFAULT_INDICATOR_IN_FRONT = false; + private static final boolean DEFAULT_INDICATOR_WITHOUT_PADDING = false; + private static final int DEFAULT_INDICATOR_GRAVITY = GRAVITY_BOTTOM; + private static final boolean DEFAULT_DRAW_DECORATION_AFTER_TAB = false; + + private final int topBorderThickness; + private final int topBorderColor; + private final int bottomBorderThickness; + private final int bottomBorderColor; + private final Paint borderPaint; + private final RectF indicatorRectF = new RectF(); + private final boolean indicatorWithoutPadding; + private final boolean indicatorAlwaysInCenter; + private final boolean indicatorInFront; + private final int indicatorThickness; + private final int indicatorWidth; + private final int indicatorGravity; + private final float indicatorCornerRadius; + private final Paint indicatorPaint; + private final int dividerThickness; + private final Paint dividerPaint; + private final float dividerHeight; + private final SimpleTabColorizer defaultTabColorizer; + private final boolean drawDecorationAfterTab; + + private int lastPosition; + private int selectedPosition; + private float selectionOffset; + private SmartTabIndicationInterpolator indicationInterpolator; + private TabColorizer customTabColorizer; + + SmartTabStrip(Context context, AttributeSet attrs) { + super(context); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + boolean indicatorWithoutPadding = DEFAULT_INDICATOR_WITHOUT_PADDING; + boolean indicatorInFront = DEFAULT_INDICATOR_IN_FRONT; + boolean indicatorAlwaysInCenter = DEFAULT_INDICATOR_IN_CENTER; + int indicationInterpolatorId = SmartTabIndicationInterpolator.ID_SMART; + int indicatorGravity = DEFAULT_INDICATOR_GRAVITY; + int indicatorColor = DEFAULT_SELECTED_INDICATOR_COLOR; + int indicatorColorsId = NO_ID; + int indicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + int indicatorWidth = AUTO_WIDTH; + float indicatorCornerRadius = DEFAULT_INDICATOR_CORNER_RADIUS * density; + int overlineColor = setColorAlpha(themeForegroundColor, DEFAULT_TOP_BORDER_COLOR_ALPHA); + int overlineThickness = (int) (DEFAULT_TOP_BORDER_THICKNESS_DIPS * density); + int underlineColor = setColorAlpha(themeForegroundColor, DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + int underlineThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + int dividerColor = setColorAlpha(themeForegroundColor, DEFAULT_DIVIDER_COLOR_ALPHA); + int dividerColorsId = NO_ID; + int dividerThickness = (int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density); + boolean drawDecorationAfterTab = DEFAULT_DRAW_DECORATION_AFTER_TAB; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmartTabLayout); + indicatorAlwaysInCenter = a.getBoolean( + R.styleable.SmartTabLayout_stl_indicatorAlwaysInCenter, indicatorAlwaysInCenter); + indicatorWithoutPadding = a.getBoolean( + R.styleable.SmartTabLayout_stl_indicatorWithoutPadding, indicatorWithoutPadding); + indicatorInFront = a.getBoolean( + R.styleable.SmartTabLayout_stl_indicatorInFront, indicatorInFront); + indicationInterpolatorId = a.getInt( + R.styleable.SmartTabLayout_stl_indicatorInterpolation, indicationInterpolatorId); + indicatorGravity = a.getInt( + R.styleable.SmartTabLayout_stl_indicatorGravity, indicatorGravity); + indicatorColor = a.getColor( + R.styleable.SmartTabLayout_stl_indicatorColor, indicatorColor); + indicatorColorsId = a.getResourceId( + R.styleable.SmartTabLayout_stl_indicatorColors, indicatorColorsId); + indicatorThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_indicatorThickness, indicatorThickness); + indicatorWidth = a.getLayoutDimension( + R.styleable.SmartTabLayout_stl_indicatorWidth, indicatorWidth); + indicatorCornerRadius = a.getDimension( + R.styleable.SmartTabLayout_stl_indicatorCornerRadius, indicatorCornerRadius); + overlineColor = a.getColor( + R.styleable.SmartTabLayout_stl_overlineColor, overlineColor); + overlineThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_overlineThickness, overlineThickness); + underlineColor = a.getColor( + R.styleable.SmartTabLayout_stl_underlineColor, underlineColor); + underlineThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_underlineThickness, underlineThickness); + dividerColor = a.getColor( + R.styleable.SmartTabLayout_stl_dividerColor, dividerColor); + dividerColorsId = a.getResourceId( + R.styleable.SmartTabLayout_stl_dividerColors, dividerColorsId); + dividerThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout_stl_dividerThickness, dividerThickness); + drawDecorationAfterTab = a.getBoolean( + R.styleable.SmartTabLayout_stl_drawDecorationAfterTab, drawDecorationAfterTab); + a.recycle(); + + final int[] indicatorColors = (indicatorColorsId == NO_ID) + ? new int[] { indicatorColor } + : getResources().getIntArray(indicatorColorsId); + + final int[] dividerColors = (dividerColorsId == NO_ID) + ? new int[] { dividerColor } + : getResources().getIntArray(dividerColorsId); + + this.defaultTabColorizer = new SimpleTabColorizer(); + this.defaultTabColorizer.setIndicatorColors(indicatorColors); + this.defaultTabColorizer.setDividerColors(dividerColors); + + this.topBorderThickness = overlineThickness; + this.topBorderColor = overlineColor; + this.bottomBorderThickness = underlineThickness; + this.bottomBorderColor = underlineColor; + this.borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + this.indicatorAlwaysInCenter = indicatorAlwaysInCenter; + this.indicatorWithoutPadding = indicatorWithoutPadding; + this.indicatorInFront = indicatorInFront; + this.indicatorThickness = indicatorThickness; + this.indicatorWidth = indicatorWidth; + this.indicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.indicatorCornerRadius = indicatorCornerRadius; + this.indicatorGravity = indicatorGravity; + + this.dividerHeight = DEFAULT_DIVIDER_HEIGHT; + this.dividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.dividerPaint.setStrokeWidth(dividerThickness); + this.dividerThickness = dividerThickness; + + this.drawDecorationAfterTab = drawDecorationAfterTab; + + this.indicationInterpolator = SmartTabIndicationInterpolator.of(indicationInterpolatorId); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + void setIndicationInterpolator(SmartTabIndicationInterpolator interpolator) { + indicationInterpolator = interpolator; + invalidate(); + } + + void setCustomTabColorizer(TabColorizer customTabColorizer) { + this.customTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + customTabColorizer = null; + defaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + customTabColorizer = null; + defaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + selectedPosition = position; + selectionOffset = positionOffset; + if (positionOffset == 0f && lastPosition != selectedPosition) { + lastPosition = selectedPosition; + } + invalidate(); + } + + boolean isIndicatorAlwaysInCenter() { + return indicatorAlwaysInCenter; + } + + TabColorizer getTabColorizer() { + return (customTabColorizer != null) ? customTabColorizer : defaultTabColorizer; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!drawDecorationAfterTab) { + drawDecoration(canvas); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (drawDecorationAfterTab) { + drawDecoration(canvas); + } + } + + private void drawDecoration(Canvas canvas) { + final int height = getHeight(); + final int width = getWidth(); + final int tabCount = getChildCount(); + final TabColorizer tabColorizer = getTabColorizer(); + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + + if (indicatorInFront) { + drawOverline(canvas, 0, width); + drawUnderline(canvas, 0, width, height); + } + + // Thick colored underline below the current selection + if (tabCount > 0) { + View selectedTab = getChildAt(selectedPosition); + int selectedStart = Utils.getStart(selectedTab, indicatorWithoutPadding); + int selectedEnd = Utils.getEnd(selectedTab, indicatorWithoutPadding); + int left; + int right; + if (isLayoutRtl) { + left = selectedEnd; + right = selectedStart; + } else { + left = selectedStart; + right = selectedEnd; + } + + int color = tabColorizer.getIndicatorColor(selectedPosition); + float thickness = indicatorThickness; + + if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(selectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, selectionOffset); + } + + // Draw the selection partway between the tabs + float startOffset = indicationInterpolator.getLeftEdge(selectionOffset); + float endOffset = indicationInterpolator.getRightEdge(selectionOffset); + float thicknessOffset = indicationInterpolator.getThickness(selectionOffset); + + View nextTab = getChildAt(selectedPosition + 1); + int nextStart = Utils.getStart(nextTab, indicatorWithoutPadding); + int nextEnd = Utils.getEnd(nextTab, indicatorWithoutPadding); + if (isLayoutRtl) { + left = (int) (endOffset * nextEnd + (1.0f - endOffset) * left); + right = (int) (startOffset * nextStart + (1.0f - startOffset) * right); + } else { + left = (int) (startOffset * nextStart + (1.0f - startOffset) * left); + right = (int) (endOffset * nextEnd + (1.0f - endOffset) * right); + } + thickness = thickness * thicknessOffset; + } + + drawIndicator(canvas, left, right, height, thickness, color); + + } + + if (!indicatorInFront) { + drawOverline(canvas, 0, width); + drawUnderline(canvas, 0, getWidth(), height); + } + + // Vertical separators between the titles + drawSeparator(canvas, height, tabCount); + + } + + private void drawSeparator(Canvas canvas, int height, int tabCount) { + if (dividerThickness <= 0) { + return; + } + + final int dividerHeightPx = (int) (Math.min(Math.max(0f, dividerHeight), 1f) * height); + final TabColorizer tabColorizer = getTabColorizer(); + + // Vertical separators between the titles + final int separatorTop = (height - dividerHeightPx) / 2; + final int separatorBottom = separatorTop + dividerHeightPx; + + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + for (int i = 0; i < tabCount - 1; i++) { + View child = getChildAt(i); + int end = Utils.getEnd(child); + int endMargin = Utils.getMarginEnd(child); + int separatorX = isLayoutRtl ? end - endMargin : end + endMargin; + dividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(separatorX, separatorTop, separatorX, separatorBottom, dividerPaint); + } + } + + private void drawIndicator(Canvas canvas, int left, int right, int height, float thickness, + int color) { + if (indicatorThickness <= 0 || indicatorWidth == 0) { + return; + } + + float center; + float top; + float bottom; + + switch (indicatorGravity) { + case GRAVITY_TOP: + center = indicatorThickness / 2f; + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + break; + case GRAVITY_CENTER: + center = height / 2f; + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + break; + case GRAVITY_BOTTOM: + default: + center = height - (indicatorThickness / 2f); + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + } + + indicatorPaint.setColor(color); + if (indicatorWidth == AUTO_WIDTH) { + indicatorRectF.set(left, top, right, bottom); + } else { + float padding = (Math.abs(left - right) - indicatorWidth) / 2f; + indicatorRectF.set(left + padding, top, right - padding, bottom); + } + + if (indicatorCornerRadius > 0f) { + canvas.drawRoundRect( + indicatorRectF, indicatorCornerRadius, + indicatorCornerRadius, indicatorPaint); + } else { + canvas.drawRect(indicatorRectF, indicatorPaint); + } + } + + private void drawOverline(Canvas canvas, int left, int right) { + if (topBorderThickness <= 0) { + return; + } + // Thin overline along the entire top edge + borderPaint.setColor(topBorderColor); + canvas.drawRect(left, 0, right, topBorderThickness, borderPaint); + } + + private void drawUnderline(Canvas canvas, int left, int right, int height) { + if (bottomBorderThickness <= 0) { + return; + } + // Thin underline along the entire bottom edge + borderPaint.setColor(bottomBorderColor); + canvas.drawRect(left, height - bottomBorderThickness, right, height, borderPaint); + } + + private static class SimpleTabColorizer implements TabColorizer { + + private int[] indicatorColors; + private int[] dividerColors; + + @Override + public final int getIndicatorColor(int position) { + return indicatorColors[position % indicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return dividerColors[position % dividerColors.length]; + } + + void setIndicatorColors(int... colors) { + indicatorColors = colors; + } + + void setDividerColors(int... colors) { + dividerColors = colors; + } + } +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip2.java b/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip2.java new file mode 100644 index 0000000..d6eb19f --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/SmartTabStrip2.java @@ -0,0 +1,446 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +import com.chwl.library.R; + +/** + *

+ * Forked from Google Samples > SlidingTabsBasic > + * SlidingTabStrip + */ +class SmartTabStrip2 extends LinearLayout { + + private static final int GRAVITY_BOTTOM = 0; + private static final int GRAVITY_TOP = 1; + private static final int GRAVITY_CENTER = 2; + + private static final int AUTO_WIDTH = -1; + + private static final int DEFAULT_TOP_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_TOP_BORDER_COLOR_ALPHA = 0x26; + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + private static final float DEFAULT_INDICATOR_CORNER_RADIUS = 0f; + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + private static final boolean DEFAULT_INDICATOR_IN_CENTER = false; + private static final boolean DEFAULT_INDICATOR_IN_FRONT = false; + private static final boolean DEFAULT_INDICATOR_WITHOUT_PADDING = false; + private static final int DEFAULT_INDICATOR_GRAVITY = GRAVITY_BOTTOM; + private static final boolean DEFAULT_DRAW_DECORATION_AFTER_TAB = false; + + private final int topBorderThickness; + private final int topBorderColor; + private final int bottomBorderThickness; + private final int bottomBorderColor; + private final Paint borderPaint; + private final RectF indicatorRectF = new RectF(); + private final boolean indicatorWithoutPadding; + private final boolean indicatorAlwaysInCenter; + private final boolean indicatorInFront; + private final int indicatorThickness; + private final int indicatorWidth; + private final int indicatorGravity; + private final float indicatorCornerRadius; + private final Paint indicatorPaint; + private final int dividerThickness; + private final Paint dividerPaint; + private final float dividerHeight; + private final SimpleTabColorizer defaultTabColorizer; + private final boolean drawDecorationAfterTab; + + private int lastPosition; + private int selectedPosition; + private float selectionOffset; + private SmartTabIndicationInterpolator indicationInterpolator; + private TabColorizer customTabColorizer; + + SmartTabStrip2(Context context, AttributeSet attrs) { + super(context); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + boolean indicatorWithoutPadding = DEFAULT_INDICATOR_WITHOUT_PADDING; + boolean indicatorInFront = DEFAULT_INDICATOR_IN_FRONT; + boolean indicatorAlwaysInCenter = DEFAULT_INDICATOR_IN_CENTER; + int indicationInterpolatorId = SmartTabIndicationInterpolator.ID_SMART; + int indicatorGravity = DEFAULT_INDICATOR_GRAVITY; + int indicatorColor = DEFAULT_SELECTED_INDICATOR_COLOR; + int indicatorColorsId = NO_ID; + int indicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + int indicatorWidth = AUTO_WIDTH; + float indicatorCornerRadius = DEFAULT_INDICATOR_CORNER_RADIUS * density; + int overlineColor = setColorAlpha(themeForegroundColor, DEFAULT_TOP_BORDER_COLOR_ALPHA); + int overlineThickness = (int) (DEFAULT_TOP_BORDER_THICKNESS_DIPS * density); + int underlineColor = setColorAlpha(themeForegroundColor, DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + int underlineThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + int dividerColor = setColorAlpha(themeForegroundColor, DEFAULT_DIVIDER_COLOR_ALPHA); + int dividerColorsId = NO_ID; + int dividerThickness = (int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density); + boolean drawDecorationAfterTab = DEFAULT_DRAW_DECORATION_AFTER_TAB; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmartTabLayout2); + indicatorAlwaysInCenter = a.getBoolean( + R.styleable.SmartTabLayout2_stl_indicatorAlwaysInCenter, indicatorAlwaysInCenter); + indicatorWithoutPadding = a.getBoolean( + R.styleable.SmartTabLayout2_stl_indicatorWithoutPadding, indicatorWithoutPadding); + indicatorInFront = a.getBoolean( + R.styleable.SmartTabLayout2_stl_indicatorInFront, indicatorInFront); + indicationInterpolatorId = a.getInt( + R.styleable.SmartTabLayout2_stl_indicatorInterpolation, indicationInterpolatorId); + indicatorGravity = a.getInt( + R.styleable.SmartTabLayout2_stl_indicatorGravity, indicatorGravity); + indicatorColor = a.getColor( + R.styleable.SmartTabLayout2_stl_indicatorColor, indicatorColor); + indicatorColorsId = a.getResourceId( + R.styleable.SmartTabLayout2_stl_indicatorColors, indicatorColorsId); + indicatorThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_indicatorThickness, indicatorThickness); + indicatorWidth = a.getLayoutDimension( + R.styleable.SmartTabLayout2_stl_indicatorWidth, indicatorWidth); + indicatorCornerRadius = a.getDimension( + R.styleable.SmartTabLayout2_stl_indicatorCornerRadius, indicatorCornerRadius); + overlineColor = a.getColor( + R.styleable.SmartTabLayout2_stl_overlineColor, overlineColor); + overlineThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_overlineThickness, overlineThickness); + underlineColor = a.getColor( + R.styleable.SmartTabLayout2_stl_underlineColor, underlineColor); + underlineThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_underlineThickness, underlineThickness); + dividerColor = a.getColor( + R.styleable.SmartTabLayout2_stl_dividerColor, dividerColor); + dividerColorsId = a.getResourceId( + R.styleable.SmartTabLayout2_stl_dividerColors, dividerColorsId); + dividerThickness = a.getDimensionPixelSize( + R.styleable.SmartTabLayout2_stl_dividerThickness, dividerThickness); + drawDecorationAfterTab = a.getBoolean( + R.styleable.SmartTabLayout2_stl_drawDecorationAfterTab, drawDecorationAfterTab); + a.recycle(); + + final int[] indicatorColors = (indicatorColorsId == NO_ID) + ? new int[] { indicatorColor } + : getResources().getIntArray(indicatorColorsId); + + final int[] dividerColors = (dividerColorsId == NO_ID) + ? new int[] { dividerColor } + : getResources().getIntArray(dividerColorsId); + + this.defaultTabColorizer = new SimpleTabColorizer(); + this.defaultTabColorizer.setIndicatorColors(indicatorColors); + this.defaultTabColorizer.setDividerColors(dividerColors); + + this.topBorderThickness = overlineThickness; + this.topBorderColor = overlineColor; + this.bottomBorderThickness = underlineThickness; + this.bottomBorderColor = underlineColor; + this.borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + this.indicatorAlwaysInCenter = indicatorAlwaysInCenter; + this.indicatorWithoutPadding = indicatorWithoutPadding; + this.indicatorInFront = indicatorInFront; + this.indicatorThickness = indicatorThickness; + this.indicatorWidth = indicatorWidth; + this.indicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.indicatorCornerRadius = indicatorCornerRadius; + this.indicatorGravity = indicatorGravity; + + this.dividerHeight = DEFAULT_DIVIDER_HEIGHT; + this.dividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.dividerPaint.setStrokeWidth(dividerThickness); + this.dividerThickness = dividerThickness; + + this.drawDecorationAfterTab = drawDecorationAfterTab; + + this.indicationInterpolator = SmartTabIndicationInterpolator.of(indicationInterpolatorId); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + void setIndicationInterpolator(SmartTabIndicationInterpolator interpolator) { + indicationInterpolator = interpolator; + invalidate(); + } + + void setCustomTabColorizer(TabColorizer customTabColorizer) { + this.customTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + customTabColorizer = null; + defaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + customTabColorizer = null; + defaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + selectedPosition = position; + selectionOffset = positionOffset; + if (positionOffset == 0f && lastPosition != selectedPosition) { + lastPosition = selectedPosition; + } + invalidate(); + } + + boolean isIndicatorAlwaysInCenter() { + return indicatorAlwaysInCenter; + } + + TabColorizer getTabColorizer() { + return (customTabColorizer != null) ? customTabColorizer : defaultTabColorizer; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!drawDecorationAfterTab) { + drawDecoration(canvas); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (drawDecorationAfterTab) { + drawDecoration(canvas); + } + } + + private void drawDecoration(Canvas canvas) { + final int height = getHeight(); + final int width = getWidth(); + final int tabCount = getChildCount(); + final TabColorizer tabColorizer = getTabColorizer(); + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + + if (indicatorInFront) { + drawOverline(canvas, 0, width); + drawUnderline(canvas, 0, width, height); + } + + // Thick colored underline below the current selection + if (tabCount > 0) { + View selectedTab = getChildAt(selectedPosition); + int selectedStart = Utils.getStart(selectedTab, indicatorWithoutPadding); + int selectedEnd = Utils.getEnd(selectedTab, indicatorWithoutPadding); + int left; + int right; + if (isLayoutRtl) { + left = selectedEnd; + right = selectedStart; + } else { + left = selectedStart; + right = selectedEnd; + } + + int color = tabColorizer.getIndicatorColor(selectedPosition); + float thickness = indicatorThickness; + + if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(selectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, selectionOffset); + } + + // Draw the selection partway between the tabs + float startOffset = indicationInterpolator.getLeftEdge(selectionOffset); + float endOffset = indicationInterpolator.getRightEdge(selectionOffset); + float thicknessOffset = indicationInterpolator.getThickness(selectionOffset); + + View nextTab = getChildAt(selectedPosition + 1); + int nextStart = Utils.getStart(nextTab, indicatorWithoutPadding); + int nextEnd = Utils.getEnd(nextTab, indicatorWithoutPadding); + if (isLayoutRtl) { + left = (int) (endOffset * nextEnd + (1.0f - endOffset) * left); + right = (int) (startOffset * nextStart + (1.0f - startOffset) * right); + } else { + left = (int) (startOffset * nextStart + (1.0f - startOffset) * left); + right = (int) (endOffset * nextEnd + (1.0f - endOffset) * right); + } + thickness = thickness * thicknessOffset; + } + + drawIndicator(canvas, left, right, height, thickness, color); + + } + + if (!indicatorInFront) { + drawOverline(canvas, 0, width); + drawUnderline(canvas, 0, getWidth(), height); + } + + // Vertical separators between the titles + drawSeparator(canvas, height, tabCount); + + } + + private void drawSeparator(Canvas canvas, int height, int tabCount) { + if (dividerThickness <= 0) { + return; + } + + final int dividerHeightPx = (int) (Math.min(Math.max(0f, dividerHeight), 1f) * height); + final TabColorizer tabColorizer = getTabColorizer(); + + // Vertical separators between the titles + final int separatorTop = (height - dividerHeightPx) / 2; + final int separatorBottom = separatorTop + dividerHeightPx; + + final boolean isLayoutRtl = Utils.isLayoutRtl(this); + for (int i = 0; i < tabCount - 1; i++) { + View child = getChildAt(i); + int end = Utils.getEnd(child); + int endMargin = Utils.getMarginEnd(child); + int separatorX = isLayoutRtl ? end - endMargin : end + endMargin; + dividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(separatorX, separatorTop, separatorX, separatorBottom, dividerPaint); + } + } + + private void drawIndicator(Canvas canvas, int left, int right, int height, float thickness, + int color) { + if (indicatorThickness <= 0 || indicatorWidth == 0) { + return; + } + + float center; + float top; + float bottom; + + switch (indicatorGravity) { + case GRAVITY_TOP: + center = indicatorThickness / 2f; + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + break; + case GRAVITY_CENTER: + center = height / 2f; + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + break; + case GRAVITY_BOTTOM: + default: + center = height - (indicatorThickness / 2f); + top = center - (thickness / 2f); + bottom = center + (thickness / 2f); + } + + indicatorPaint.setColor(color); + if (indicatorWidth == AUTO_WIDTH) { + indicatorRectF.set(left, top, right, bottom); + } else { + float padding = (Math.abs(left - right) - indicatorWidth) / 2f; + indicatorRectF.set(left + padding, top, right - padding, bottom); + } + + if (indicatorCornerRadius > 0f) { + canvas.drawRoundRect( + indicatorRectF, indicatorCornerRadius, + indicatorCornerRadius, indicatorPaint); + } else { + canvas.drawRect(indicatorRectF, indicatorPaint); + } + } + + private void drawOverline(Canvas canvas, int left, int right) { + if (topBorderThickness <= 0) { + return; + } + // Thin overline along the entire top edge + borderPaint.setColor(topBorderColor); + canvas.drawRect(left, 0, right, topBorderThickness, borderPaint); + } + + private void drawUnderline(Canvas canvas, int left, int right, int height) { + if (bottomBorderThickness <= 0) { + return; + } + // Thin underline along the entire bottom edge + borderPaint.setColor(bottomBorderColor); + canvas.drawRect(left, height - bottomBorderThickness, right, height, borderPaint); + } + + private static class SimpleTabColorizer implements TabColorizer { + + private int[] indicatorColors; + private int[] dividerColors; + + @Override + public final int getIndicatorColor(int position) { + return indicatorColors[position % indicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return dividerColors[position % dividerColors.length]; + } + + void setIndicatorColors(int... colors) { + indicatorColors = colors; + } + + void setDividerColors(int... colors) { + dividerColors = colors; + } + } +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/TabColorizer.java b/library/src/main/java/com/chwl/library/widget/tab/TabColorizer.java new file mode 100644 index 0000000..24276ea --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/TabColorizer.java @@ -0,0 +1,19 @@ +package com.chwl.library.widget.tab; + +/** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link SmartTabLayout#setCustomTabColorizer(TabColorizer)}. + */ +public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/TabTitleProvider.java b/library/src/main/java/com/chwl/library/widget/tab/TabTitleProvider.java new file mode 100644 index 0000000..a974341 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/TabTitleProvider.java @@ -0,0 +1,8 @@ +package com.chwl.library.widget.tab; + +import androidx.annotation.Nullable; + +public interface TabTitleProvider { + @Nullable + CharSequence getPageTitle(int position); +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/Utils.java b/library/src/main/java/com/chwl/library/widget/tab/Utils.java new file mode 100644 index 0000000..e5e5528 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/Utils.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2015 ogaclejapan + * Copyright (C) 2013 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.chwl.library.widget.tab; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.core.view.MarginLayoutParamsCompat; +import androidx.core.view.ViewCompat; + +final class Utils { + + private Utils() { } + + static int getMeasuredWidth(View v) { + return (v == null) ? 0 : v.getMeasuredWidth(); + } + + static int getWidth(View v) { + return (v == null) ? 0 : v.getWidth(); + } + + static int getWidthWithMargin(View v) { + return getWidth(v) + getMarginHorizontally(v); + } + + static int getStart(View v) { + return getStart(v, false); + } + + static int getStart(View v, boolean withoutPadding) { + if (v == null) { + return 0; + } + if (isLayoutRtl(v)) { + return (withoutPadding) ? v.getRight() - getPaddingStart(v) : v.getRight(); + } else { + return (withoutPadding) ? v.getLeft() + getPaddingStart(v) : v.getLeft(); + } + } + + static int getEnd(View v) { + return getEnd(v, false); + } + + static int getEnd(View v, boolean withoutPadding) { + if (v == null) { + return 0; + } + if (isLayoutRtl(v)) { + return (withoutPadding) ? v.getLeft() + getPaddingEnd(v) : v.getLeft(); + } else { + return (withoutPadding) ? v.getRight() - getPaddingEnd(v) : v.getRight(); + } + } + + static int getPaddingStart(View v) { + if (v == null) { + return 0; + } + return ViewCompat.getPaddingStart(v); + } + + static int getPaddingEnd(View v) { + if (v == null) { + return 0; + } + return ViewCompat.getPaddingEnd(v); + } + + static int getPaddingHorizontally(View v) { + if (v == null) { + return 0; + } + return v.getPaddingLeft() + v.getPaddingRight(); + } + + static int getMarginStart(View v) { + if (v == null) { + return 0; + } + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + return MarginLayoutParamsCompat.getMarginStart(lp); + } + + static int getMarginEnd(View v) { + if (v == null) { + return 0; + } + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + return MarginLayoutParamsCompat.getMarginEnd(lp); + } + + static int getMarginHorizontally(View v) { + if (v == null) { + return 0; + } + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + return MarginLayoutParamsCompat.getMarginStart(lp) + MarginLayoutParamsCompat.getMarginEnd(lp); + } + + static boolean isLayoutRtl(View v) { + return ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_RTL; + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/FragmentPageAdapter.kt b/library/src/main/java/com/chwl/library/widget/tab/util/FragmentPageAdapter.kt new file mode 100644 index 0000000..5e60839 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/FragmentPageAdapter.kt @@ -0,0 +1,35 @@ +package com.chwl.library.widget.tab.util + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter + +class FragmentPageAdapter : FragmentStateAdapter { + + var mFgList: List? = null + + constructor(fragmentActivity: FragmentActivity, fgList: List?) : super(fragmentActivity){ + mFgList = fgList + } + constructor(fragment: Fragment, fgList: List?) : super(fragment){ + mFgList = fgList + } + constructor(fragmentManager: FragmentManager, lifecycle: Lifecycle, fgList: List?) : super( + fragmentManager, + lifecycle + ){ + mFgList = fgList + } + + + override fun getItemCount(): Int { + return mFgList?.size?:0 + } + + override fun createFragment(position: Int): Fragment { + return mFgList?.get(position) ?: Fragment() + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/PagerItem.java b/library/src/main/java/com/chwl/library/widget/tab/util/PagerItem.java new file mode 100644 index 0000000..69772b2 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/PagerItem.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util; + +public abstract class PagerItem { + + protected static final float DEFAULT_WIDTH = 1.f; + + private final CharSequence title; + private final float width; + + protected PagerItem(CharSequence title, float width) { + this.title = title; + this.width = width; + } + + public CharSequence getTitle() { + return title; + } + + public float getWidth() { + return width; + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/PagerItems.java b/library/src/main/java/com/chwl/library/widget/tab/util/PagerItems.java new file mode 100644 index 0000000..6a96365 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/PagerItems.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util; + +import android.content.Context; + +import java.util.ArrayList; + +public abstract class PagerItems extends ArrayList { + + private final Context context; + + protected PagerItems(Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItem.java b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItem.java new file mode 100644 index 0000000..ab5eabd --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItem.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; + +public class ViewPagerItem extends PagerItem { + + private final int resource; + + protected ViewPagerItem(CharSequence title, float width, @LayoutRes int resource) { + super(title, width); + this.resource = resource; + } + + public static ViewPagerItem of(CharSequence title, @LayoutRes int resource) { + return of(title, DEFAULT_WIDTH, resource); + } + + public static ViewPagerItem of(CharSequence title, float width, @LayoutRes int resource) { + return new ViewPagerItem(title, width, resource); + } + + public View initiate(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(resource, container, false); + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItemAdapter.java b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItemAdapter.java new file mode 100644 index 0000000..4dc50fa --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItemAdapter.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; +import androidx.viewpager.widget.PagerAdapter; + +import java.lang.ref.WeakReference; + +public class ViewPagerItemAdapter extends PagerAdapter { + + private final ViewPagerItems pages; + private final SparseArrayCompat> holder; + private final LayoutInflater inflater; + + public ViewPagerItemAdapter(ViewPagerItems pages) { + this.pages = pages; + this.holder = new SparseArrayCompat<>(pages.size()); + this.inflater = LayoutInflater.from(pages.getContext()); + } + + @Override + public int getCount() { + return pages.size(); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + View view = getPagerItem(position).initiate(inflater, container); + container.addView(view); + holder.put(position, new WeakReference(view)); + return view; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + holder.remove(position); + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return object == view; + } + + @Override + public CharSequence getPageTitle(int position) { + return getPagerItem(position).getTitle(); + } + + @Override + public float getPageWidth(int position) { + return getPagerItem(position).getWidth(); + } + + public View getPage(int position) { + final WeakReference weakRefItem = holder.get(position); + return (weakRefItem != null) ? weakRefItem.get() : null; + } + + protected ViewPagerItem getPagerItem(int position) { + return pages.get(position); + } +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItems.java b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItems.java new file mode 100644 index 0000000..e456e9d --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/ViewPagerItems.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util; + +import android.content.Context; + +import androidx.annotation.LayoutRes; +import androidx.annotation.StringRes; + +public class ViewPagerItems extends PagerItems { + + public ViewPagerItems(Context context) { + super(context); + } + + public static Creator with(Context context) { + return new Creator(context); + } + + public static class Creator { + + private final ViewPagerItems items; + + public Creator(Context context) { + items = new ViewPagerItems(context); + } + + public Creator add(@StringRes int title, @LayoutRes int resource) { + return add(ViewPagerItem.of(items.getContext().getString(title), resource)); + } + + public Creator add(@StringRes int title, float width, @LayoutRes int resource) { + return add(ViewPagerItem.of(items.getContext().getString(title), width, resource)); + } + + public Creator add(CharSequence title, @LayoutRes int resource) { + return add(ViewPagerItem.of(title, resource)); + } + + public Creator add(ViewPagerItem item) { + items.add(item); + return this; + } + + public ViewPagerItems create() { + return items; + } + + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/v4/Bundler.java b/library/src/main/java/com/chwl/library/widget/tab/util/v4/Bundler.java new file mode 100644 index 0000000..527ae53 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/v4/Bundler.java @@ -0,0 +1,520 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util.v4; + +import android.annotation.TargetApi; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.util.Size; +import android.util.SizeF; +import android.util.SparseArray; + +import androidx.fragment.app.Fragment; + +import java.io.Serializable; +import java.util.ArrayList; + +public class Bundler { + + private final Bundle bundle; + + /** + * Constructs a new, empty Bundle. + */ + public Bundler() { + this(null); + } + + private Bundler(Bundle b) { + bundle = (b == null) ? new Bundle() : new Bundle(b); + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * Bundle. + * + * @param b a Bundle to be copied. + */ + public static Bundler of(Bundle b) { + return new Bundler(b); + } + + /** + * Inserts all mappings from the given Bundle into this Bundle. + * + * @param bundle a Bundle + * @return this + */ + public Bundler putAll(Bundle bundle) { + this.bundle.putAll(bundle); + return this; + } + + /** + * Inserts a byte value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a byte + * @return this + */ + public Bundler putByte(String key, byte value) { + bundle.putByte(key, value); + return this; + } + + /** + * Inserts a char value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a char, or null + * @return this + */ + public Bundler putChar(String key, char value) { + bundle.putChar(key, value); + return this; + } + + /** + * Inserts a short value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a short + * @return this + */ + public Bundler putShort(String key, short value) { + bundle.putShort(key, value); + return this; + } + + /** + * Inserts a float value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a float + * @return this + */ + public Bundler putFloat(String key, float value) { + bundle.putFloat(key, value); + return this; + } + + /** + * Inserts a CharSequence value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence, or null + * @return this + */ + public Bundler putCharSequence(String key, CharSequence value) { + bundle.putCharSequence(key, value); + return this; + } + + /** + * Inserts a Parcelable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Parcelable object, or null + * @return this + */ + public Bundler putParcelable(String key, Parcelable value) { + bundle.putParcelable(key, value); + return this; + } + + /** + * Inserts a Size value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Size object, or null + * @return this + */ + @TargetApi(21) + public Bundler putSize(String key, Size value) { + bundle.putSize(key, value); + return this; + } + + /** + * Inserts a SizeF value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a SizeF object, or null + * @return this + */ + @TargetApi(21) + public Bundler putSizeF(String key, SizeF value) { + bundle.putSizeF(key, value); + return this; + } + + /** + * Inserts an array of Parcelable values into the mapping of this Bundle, + * replacing any existing value for the given key. Either key or value may + * be null. + * + * @param key a String, or null + * @param value an array of Parcelable objects, or null + * @return this + */ + public Bundler putParcelableArray(String key, Parcelable[] value) { + bundle.putParcelableArray(key, value); + return this; + } + + /** + * Inserts a List of Parcelable values into the mapping of this Bundle, + * replacing any existing value for the given key. Either key or value may + * be null. + * + * @param key a String, or null + * @param value an ArrayList of Parcelable objects, or null + * @return this + */ + public Bundler putParcelableArrayList(String key, + ArrayList value) { + bundle.putParcelableArrayList(key, value); + return this; + } + + /** + * Inserts a SparceArray of Parcelable values into the mapping of this + * Bundle, replacing any existing value for the given key. Either key + * or value may be null. + * + * @param key a String, or null + * @param value a SparseArray of Parcelable objects, or null + * @return this + */ + public Bundler putSparseParcelableArray(String key, + SparseArray value) { + bundle.putSparseParcelableArray(key, value); + return this; + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + * @return this + */ + public Bundler putIntegerArrayList(String key, ArrayList value) { + bundle.putIntegerArrayList(key, value); + return this; + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + * @return this + */ + public Bundler putStringArrayList(String key, ArrayList value) { + bundle.putStringArrayList(key, value); + return this; + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + * @return this + */ + @TargetApi(8) + public Bundler putCharSequenceArrayList(String key, ArrayList value) { + bundle.putCharSequenceArrayList(key, value); + return this; + } + + /** + * Inserts a Serializable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Serializable object, or null + * @return this + */ + public Bundler putSerializable(String key, Serializable value) { + bundle.putSerializable(key, value); + return this; + } + + /** + * Inserts a byte array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a byte array object, or null + * @return this + */ + public Bundler putByteArray(String key, byte[] value) { + bundle.putByteArray(key, value); + return this; + } + + /** + * Inserts a short array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a short array object, or null + * @return this + */ + public Bundler putShortArray(String key, short[] value) { + bundle.putShortArray(key, value); + return this; + } + + /** + * Inserts a char array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a char array object, or null + * @return this + */ + public Bundler putCharArray(String key, char[] value) { + bundle.putCharArray(key, value); + return this; + } + + /** + * Inserts a float array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a float array object, or null + * @return this + */ + public Bundler putFloatArray(String key, float[] value) { + bundle.putFloatArray(key, value); + return this; + } + + /** + * Inserts a CharSequence array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence array object, or null + * @return this + */ + @TargetApi(8) + public Bundler putCharSequenceArray(String key, CharSequence[] value) { + bundle.putCharSequenceArray(key, value); + return this; + } + + /** + * Inserts a Bundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + * @return this + */ + public Bundler putBundle(String key, Bundle value) { + bundle.putBundle(key, value); + return this; + } + + /** + * Inserts an {@link IBinder} value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + *

You should be very careful when using this function. In many + * places where Bundles are used (such as inside of Intent objects), the Bundle + * can live longer inside of another process than the process that had originally + * created it. In that case, the IBinder you supply here will become invalid + * when your process goes away, and no longer usable, even if a new process is + * created for you later on.

+ * + * @param key a String, or null + * @param value an IBinder object, or null + * @return this + */ + @TargetApi(18) + public Bundler putBinder(String key, IBinder value) { + bundle.putBinder(key, value); + return this; + } + + /** + * Inserts a Boolean value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Boolean, or null + * @return this + */ + public Bundler putBoolean(String key, boolean value) { + bundle.putBoolean(key, value); + return this; + } + + /** + * Inserts an int value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value an int, or null + * @return this + */ + public Bundler putInt(String key, int value) { + bundle.putInt(key, value); + return this; + } + + /** + * Inserts a long value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a long + * @return this + */ + public Bundler putLong(String key, long value) { + bundle.putLong(key, value); + return this; + } + + /** + * Inserts a double value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a double + * @return this + */ + public Bundler putDouble(String key, double value) { + bundle.putDouble(key, value); + return this; + } + + /** + * Inserts a String value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String, or null + * @return this + */ + public Bundler putString(String key, String value) { + bundle.putString(key, value); + return this; + } + + /** + * Inserts a boolean array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a boolean array object, or null + * @return this + */ + public Bundler putBooleanArray(String key, boolean[] value) { + bundle.putBooleanArray(key, value); + return this; + } + + /** + * Inserts an int array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an int array object, or null + * @return this + */ + public Bundler putIntArray(String key, int[] value) { + bundle.putIntArray(key, value); + return this; + } + + /** + * Inserts a long array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a long array object, or null + * @return this + */ + public Bundler putLongArray(String key, long[] value) { + bundle.putLongArray(key, value); + return this; + } + + /** + * Inserts a double array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a double array object, or null + * @return this + */ + public Bundler putDoubleArray(String key, double[] value) { + bundle.putDoubleArray(key, value); + return this; + } + + /** + * Inserts a String array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String array object, or null + * @return this + */ + public Bundler putStringArray(String key, String[] value) { + bundle.putStringArray(key, value); + return this; + } + + /** + * Get the bundle. + * + * @return a bundle + */ + public Bundle get() { + return bundle; + } + + /** + * Set the argument of Fragment. + * + * @param fragment a fragment + * @return a fragment + */ + public T into(T fragment) { + fragment.setArguments(get()); + return fragment; + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItem.java b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItem.java new file mode 100644 index 0000000..7ca6266 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItem.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util.v4; + +import android.content.Context; +import android.os.Bundle; + +import androidx.fragment.app.Fragment; + +import com.chwl.library.widget.tab.util.PagerItem; + + +public class FragmentPagerItem extends PagerItem { + + private static final String TAG = "FragmentPagerItem"; + private static final String KEY_POSITION = TAG + ":Position"; + + private final String className; + private final Bundle args; + + protected FragmentPagerItem(CharSequence title, float width, String className, Bundle args) { + super(title, width); + this.className = className; + this.args = args; + } + + public static FragmentPagerItem of(CharSequence title, Class clazz) { + return of(title, DEFAULT_WIDTH, clazz); + } + + public static FragmentPagerItem of(CharSequence title, Class clazz, + Bundle args) { + return of(title, DEFAULT_WIDTH, clazz, args); + } + + public static FragmentPagerItem of(CharSequence title, float width, + Class clazz) { + return of(title, width, clazz, new Bundle()); + } + + public static FragmentPagerItem of(CharSequence title, float width, + Class clazz, Bundle args) { + return new FragmentPagerItem(title, width, clazz.getName(), args); + } + + public static boolean hasPosition(Bundle args) { + return args != null && args.containsKey(KEY_POSITION); + } + + public static int getPosition(Bundle args) { + return (hasPosition(args)) ? args.getInt(KEY_POSITION) : 0; + } + + static void setPosition(Bundle args, int position) { + args.putInt(KEY_POSITION, position); + } + + public Fragment instantiate(Context context, int position) { + setPosition(args, position); + return Fragment.instantiate(context, className, args); + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItemAdapter.java b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItemAdapter.java new file mode 100644 index 0000000..dd99fdf --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItemAdapter.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util.v4; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import java.lang.ref.WeakReference; + +public class FragmentPagerItemAdapter extends FragmentPagerAdapter { + + private final FragmentPagerItems pages; + private final SparseArrayCompat> holder; + + public FragmentPagerItemAdapter(FragmentManager fm, FragmentPagerItems pages) { + super(fm); + this.pages = pages; + this.holder = new SparseArrayCompat<>(pages.size()); + } + + @Override + public int getCount() { + return pages.size(); + } + + @Override + public Fragment getItem(int position) { + return getPagerItem(position).instantiate(pages.getContext(), position); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + Object item = super.instantiateItem(container, position); + if (item instanceof Fragment) { + holder.put(position, new WeakReference((Fragment) item)); + } + return item; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + holder.remove(position); + super.destroyItem(container, position, object); + } + + @Override + public CharSequence getPageTitle(int position) { + return getPagerItem(position).getTitle(); + } + + @Override + public float getPageWidth(int position) { + return super.getPageWidth(position); + } + + public Fragment getPage(int position) { + final WeakReference weakRefItem = holder.get(position); + return (weakRefItem != null) ? weakRefItem.get() : null; + } + + protected FragmentPagerItem getPagerItem(int position) { + return pages.get(position); + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItems.java b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItems.java new file mode 100644 index 0000000..c5c9877 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentPagerItems.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util.v4; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.chwl.library.widget.tab.util.PagerItems; + + +public class FragmentPagerItems extends PagerItems { + + public FragmentPagerItems(Context context) { + super(context); + } + + public static Creator with(Context context) { + return new Creator(context); + } + + public static class Creator { + + private final FragmentPagerItems items; + + public Creator(Context context) { + items = new FragmentPagerItems(context); + } + + public Creator add(@StringRes int title, Class clazz) { + return add(FragmentPagerItem.of(items.getContext().getString(title), clazz)); + } + + public Creator add(@StringRes int title, Class clazz, Bundle args) { + return add(FragmentPagerItem.of(items.getContext().getString(title), clazz, args)); + } + + public Creator add(@StringRes int title, float width, Class clazz) { + return add(FragmentPagerItem.of(items.getContext().getString(title), width, clazz)); + } + + public Creator add(@StringRes int title, float width, Class clazz, + Bundle args) { + return add(FragmentPagerItem.of(items.getContext().getString(title), width, clazz, args)); + } + + public Creator add(CharSequence title, Class clazz) { + return add(FragmentPagerItem.of(title, clazz)); + } + + public Creator add(CharSequence title, Class clazz, Bundle args) { + return add(FragmentPagerItem.of(title, clazz, args)); + } + + public Creator add(FragmentPagerItem item) { + items.add(item); + return this; + } + + public FragmentPagerItems create() { + return items; + } + + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentStatePagerItemAdapter.java b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentStatePagerItemAdapter.java new file mode 100644 index 0000000..fe29eb7 --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/tab/util/v4/FragmentStatePagerItemAdapter.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2015 ogaclejapan + * + * 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.chwl.library.widget.tab.util.v4; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; + +import java.lang.ref.WeakReference; + +public class FragmentStatePagerItemAdapter extends FragmentStatePagerAdapter { + + private final FragmentPagerItems pages; + private final SparseArrayCompat> holder; + + public FragmentStatePagerItemAdapter(FragmentManager fm, FragmentPagerItems pages) { + super(fm); + this.pages = pages; + this.holder = new SparseArrayCompat<>(pages.size()); + } + + @Override + public int getCount() { + return pages.size(); + } + + @Override + public Fragment getItem(int position) { + return getPagerItem(position).instantiate(pages.getContext(), position); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + Object item = super.instantiateItem(container, position); + if (item instanceof Fragment) { + holder.put(position, new WeakReference((Fragment) item)); + } + return item; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + holder.remove(position); + super.destroyItem(container, position, object); + } + + @Override + public CharSequence getPageTitle(int position) { + return getPagerItem(position).getTitle(); + } + + @Override + public float getPageWidth(int position) { + return getPagerItem(position).getWidth(); + } + + public Fragment getPage(int position) { + final WeakReference weakRefItem = holder.get(position); + return (weakRefItem != null) ? weakRefItem.get() : null; + } + + protected FragmentPagerItem getPagerItem(int position) { + return pages.get(position); + } + +} diff --git a/library/src/main/java/com/chwl/library/widget/text/DrawableTextView.java b/library/src/main/java/com/chwl/library/widget/text/DrawableTextView.java new file mode 100644 index 0000000..53e772a --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/text/DrawableTextView.java @@ -0,0 +1,791 @@ +package com.chwl.library.widget.text; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.chwl.library.R; +import com.chwl.library.common.util.OtherExtKt; + + +/** + * 特殊的 AppCompatTextView + * 包括圆角、渐变色、边框、填充色等设置 + * Created by 11876 on 2018/3/15. + */ + +public class DrawableTextView extends AppCompatTextView { + //-1 无特殊drawable 0 图形背景 矩形 圆角 1 图层背景 点击背景变换 2 渐变背景 + private int drawableType = -1; + //View 的形状 -- 各种圆角还是非圆角 + private float radius = 0, leftTopRadius = 0, leftBottomRadius = 0, rightTopRadius = 0, rightBottomRadius = 0; + + //默认背景的外围线条粗细和颜色,以及内部填充颜色 + private int strikeColor = -1; + private int strikeWidth = 0; + private int soildColor = 0xffffffff; + + //STATE LIST 各种状态的情况 + private final int STATE_LIST_NONE = -1;// + private final int STATE_LIST_FALSE = 0; + private final int STATE_LIST_TRUE = 1; + //enable 状态参数 + private int STATE_ENABLE = STATE_LIST_NONE; + private int enableStrikeColor = -1; + private int enableStrikeWidth = 0; + private int enableSoildColor = 0xffffffff; + //select 状态参数 + private int STATE_SELECT = STATE_LIST_NONE; + private int selectStrikeColor = -1; + private int selectStrikeWidth = 0; + private int selectSoildColor = 0xffffffff; + //check状态参数 + private int STATE_CHECK = STATE_LIST_NONE; + private int checkStrikeColor = -1; + private int checkStrikeWidth = 0; + private int checkSoildColor = 0xffffffff; + //press状态参数 + private int STATE_PRESS = STATE_LIST_NONE; + private int pressStrikeColor = -1; + private int pressStrikeWidth = 0; + private int pressSoildColor = 0xffffffff; + + //渐变背景 -- startcolor、centerColor、endColor //渐变角度 + private int startColor = -1, centerColor = -1, endColor = -1, angle = 0; + + //文字描边 + private boolean mHasTextStroke; + private int mTextStrokeColor, mTextStrokeWidth; + private TextView mBgText = null;//用于描边的TextView + + //特殊字体 + private boolean mHasSpecialFont; + + //Drawable 相关 + public int mDrawableLeftHeight, mDrawableLeftWidth, mDrawableRightHeight, mDrawableRightWidth, mDrawableTopHeight, mDrawableTopWidth, mDrawableBottomHeight, mDrawableBottomWidth; + private Drawable mLeftDrawable, mRightDrawable, mTopDrawable, mBottomDrawable; + + private int drawableTopPadding,drawableBottomPadding,drawableLeftPadding,drawableRightPadding; + + private final boolean isInitSuper; + + //底线顶线 相关 + public static final int LINE_TOP = 1, LINE_BOTTOM = 2, LINE_TOP_AND_BOTTOM = 3; + private Paint mLinePaint; + private int mLineHeight; + private int mLineMarginLeft; + private int mLineMarginRight; + private int mLineColor; + private int mLineLocation; + + + public DrawableTextView(Context context) { + this(context, null); + } + + public DrawableTextView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + isInitSuper = true; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawableTextView); + if (a != null) { + //drawable 类型 + drawableType = a.getInt(R.styleable.DrawableTextView_dt_drawableType, -1); + //圆角相关 + radius = a.getDimension(R.styleable.DrawableTextView_dt_radius, 0); + leftTopRadius = a.getDimension(R.styleable.DrawableTextView_dt_leftTopRadius, 0); + leftBottomRadius = a.getDimension(R.styleable.DrawableTextView_dt_leftBottomRadius, 0); + rightTopRadius = a.getDimension(R.styleable.DrawableTextView_dt_rightTopRadius, 0); + rightBottomRadius = a.getDimension(R.styleable.DrawableTextView_dt_rightBottomRadius, 0); + //边框相关 + strikeColor = a.getColor(R.styleable.DrawableTextView_dt_strikeColor, -1); + strikeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_strikeWidth, 0); + //填充颜色 + soildColor = a.getColor(R.styleable.DrawableTextView_dt_soildColor, 0x00000000); + //渐变相关 + angle = a.getInt(R.styleable.DrawableTextView_dt_angle, 0); + startColor = a.getColor(R.styleable.DrawableTextView_dt_startColor, -1); + centerColor = a.getColor(R.styleable.DrawableTextView_dt_centerColor, -1); + endColor = a.getColor(R.styleable.DrawableTextView_dt_endColor, -1); + + //开启关闭状态 + STATE_ENABLE = a.getInt(R.styleable.DrawableTextView_dt_enableState, STATE_LIST_NONE); + if (STATE_ENABLE != STATE_LIST_NONE) { + enableStrikeColor = a.getColor(R.styleable.DrawableTextView_dt_enableStrikeColor, 0xffffffff); + enableStrikeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_enableStrikeWidth, 0); + enableSoildColor = a.getColor(R.styleable.DrawableTextView_dt_enableSoildColor, 0xffffffff); + } + + //按下状态 + STATE_PRESS = a.getInt(R.styleable.DrawableTextView_dt_pressState, STATE_LIST_NONE); + if (STATE_PRESS != STATE_LIST_NONE) { + pressStrikeColor = a.getColor(R.styleable.DrawableTextView_dt_pressStrikeColor, 0xffffffff); + pressStrikeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_pressStrikeWidth, 0); + pressSoildColor = a.getColor(R.styleable.DrawableTextView_dt_pressSoildColor, 0xffffffff); + } + //切换状态 + STATE_CHECK = a.getInt(R.styleable.DrawableTextView_dt_checkState, STATE_LIST_NONE); + if (STATE_CHECK != STATE_LIST_NONE) { + checkStrikeColor = a.getColor(R.styleable.DrawableTextView_dt_checkStrikeColor, 0xffffffff); + enableStrikeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_checkStrikeWidth, 0); + enableSoildColor = a.getColor(R.styleable.DrawableTextView_dt_checkSoildColor, 0xffffffff); + } + //选择状态 + STATE_SELECT = a.getInt(R.styleable.DrawableTextView_dt_selectState, STATE_LIST_NONE); + if (STATE_SELECT != STATE_LIST_NONE) { + selectStrikeColor = a.getColor(R.styleable.DrawableTextView_dt_selectStrikeColor, 0xffffffff); + selectStrikeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_selectStrikeWidth, 0); + selectSoildColor = a.getColor(R.styleable.DrawableTextView_dt_selectSoildColor, 0xffffffff); + } + + //描边相关 + mHasTextStroke = a.getBoolean(R.styleable.DrawableTextView_dt_hasTextStrike, false); + if (mHasTextStroke) { + mBgText = new TextView(context, attrs, defStyleAttr); + } + mTextStrokeWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_textStrikeWidth, 2); + mTextStrokeColor = a.getColor(R.styleable.DrawableTextView_dt_textStrikeColor, 0xFF000000); + + //特殊字体相关 + mHasSpecialFont = a.getBoolean(R.styleable.DrawableTextView_dt_hasSpecialFont, false); + if (mHasSpecialFont) { +// getPaint().setTypeface(FontUtil.getFont(context)); +// if (mHasTextStroke) { +// mBgText.getPaint().setTypeface(FontUtil.getFont(context)); +// } + } + + //图片相关 + mLeftDrawable = a.getDrawable(R.styleable.DrawableTextView_dt_drawableLeftSrc); + mTopDrawable = a.getDrawable(R.styleable.DrawableTextView_dt_drawableTopSrc); + mRightDrawable = a.getDrawable(R.styleable.DrawableTextView_dt_drawableRightSrc); + mBottomDrawable = a.getDrawable(R.styleable.DrawableTextView_dt_drawableBottomSrc); + if (mLeftDrawable != null) { + mDrawableLeftWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableLeftWidth, 0); + mDrawableLeftHeight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableLeftHeight, 0); + } + if (mTopDrawable != null) { + mDrawableTopWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableTopWidth, 0); + mDrawableTopHeight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableTopHeight, 0); + } + if (mRightDrawable != null) { + mDrawableRightWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableRightWidth, 0); + mDrawableRightHeight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableRightHeight, 0); + } + if (mBottomDrawable != null) { + mDrawableBottomWidth = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableBottomWidth, 0); + mDrawableBottomHeight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableBottomHeight, 0); + } + + if (mLeftDrawable != null || mTopDrawable != null || mRightDrawable != null || mBottomDrawable != null) { + setCompoundDrawablesWithIntrinsicBounds(mLeftDrawable, mTopDrawable, mRightDrawable, mBottomDrawable); + } + + //图片边距 + drawableLeftPadding = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableLeftPadding, 0); + drawableTopPadding = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableTopPadding, 0); + drawableRightPadding = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableRightPadding, 0); + drawableBottomPadding = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_drawableBottomPadding, 0); + + //顶线底线 相关 + mLineHeight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_lineHeight, 0); + mLineMarginLeft = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_lineMarginLeft, 0); + mLineMarginRight = a.getDimensionPixelSize(R.styleable.DrawableTextView_dt_lineMarginRight, 0); + mLineLocation = a.getInt(R.styleable.DrawableTextView_dt_lineLocation, -1); + mLineColor = a.getColor(R.styleable.DrawableTextView_dt_lineColor, -1); + } + a.recycle(); + initDrawable(context); + + //适配阿拉伯语语言环境 +// if (LanguageUtil.isRtl(context)) { + setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + setTextDirection(View.TEXT_DIRECTION_LOCALE); + // setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); +// } + } +// + +// @Override +// public void setText(CharSequence text, BufferType type) { +// CharSequence charSequence = BidiFormatter.getInstance().unicodeWrap(text, TextDirectionHeuristicsCompat.ANYRTL_LTR); +// super.setText(charSequence, type); +// } + + + + /** + * 获取参数后 开刷绘制 + * + * @param context + */ + private void initDrawable(Context context) { + switch (drawableType) { + case 0: + setBackground(getShapeDrawable()); + break; + case 1: + setBackground(getStateListDrawable()); + break; + case 2: + setGradientDrawable(context); + break; + } + } + + /** + * 有改动时调用 + */ + private void changeDrawableColor() { + switch (drawableType) { + case 0: + setBackground(getShapeDrawable()); + break; + case 1: + setBackground(getStateListDrawable()); + break; + case 2: + setGradientDrawable(getContext()); + break; + } + } + + @Override + public void setLayoutParams(ViewGroup.LayoutParams params) { + if (mHasTextStroke) { + mBgText.setLayoutParams(params); + } + super.setLayoutParams(params); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mHasTextStroke) { + mBgText.measure(widthMeasureSpec, heightMeasureSpec); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mHasTextStroke) { + mBgText.layout(left, top, right, bottom); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + //绘制背景描边 + if (mHasTextStroke) { + TextPaint textPaint = mBgText.getPaint(); + //设置描边宽度 + textPaint.setStrokeWidth(mTextStrokeWidth); + //背景描边并填充全部 + textPaint.setStyle(Paint.Style.FILL_AND_STROKE); + //设置描边颜色 + mBgText.setTextColor(mTextStrokeColor); + //将背景的文字对齐方式做同步 + mBgText.setGravity(getGravity()); + //选中粗体 + textPaint.setFakeBoldText(isSelected()); + + mBgText.draw(canvas); + } + //选中粗体 + this.getPaint().setFakeBoldText(isSelected()); + + super.onDraw(canvas); + + //绘制底线顶线 + drawLines(canvas); + } + + private GradientDrawable getShapeDrawable() { + return getShapeDrawable(strikeWidth, strikeColor, soildColor); + } + + private GradientDrawable getShapeDrawable(int strikeWidth, int strikeColor, int soildColor) { + GradientDrawable shape = new GradientDrawable(); + //设置边框,参数为边框的类型,有矩形,椭圆,还有线等等,自己去试; + shape.setShape(GradientDrawable.RECTANGLE); + //边框为矩形的时候,还可以设置边框四个角的幅度 + if (radius > 0) { + shape.setCornerRadius(radius); + } else {//*订购左上角,右上角,右下角,左下角。 + if (leftTopRadius > 0 || rightTopRadius > 0 || rightBottomRadius > 0 || leftBottomRadius > 0) { + if (OtherExtKt.isRtl(getContext())) { + shape.setCornerRadii(new float[]{rightTopRadius, rightTopRadius,leftTopRadius, leftTopRadius, leftBottomRadius, leftBottomRadius, rightBottomRadius, rightBottomRadius}); + } else { + shape.setCornerRadii(new float[]{leftTopRadius, leftTopRadius, rightTopRadius, rightTopRadius, rightBottomRadius, rightBottomRadius, leftBottomRadius, leftBottomRadius}); + } + } + } + //设置边框的厚度和颜色 + if (strikeWidth > 0) { + shape.setStroke(strikeWidth, strikeColor); + } + //填充背景颜色 + if (startColor != -1 && endColor != -1) { + shape.setOrientation(getGradientOrientation()); + if (centerColor != -1) { + shape.setColors(new int[]{startColor, centerColor, endColor}); + } else { + shape.setColors(new int[]{startColor, endColor}); + } + } else { + shape.setColor(soildColor); + } + return shape; + } + + private StateListDrawable getStateListDrawable() { + StateListDrawable stateList = new StateListDrawable(); + //获取对应的属性值 Android框架自带的属性 attr + if (STATE_PRESS != STATE_LIST_NONE) { + int press = android.R.attr.state_pressed; + stateList.addState(new int[]{STATE_PRESS == STATE_LIST_TRUE ? press : -press}, getShapeDrawable(pressStrikeWidth, pressStrikeColor, pressSoildColor)); + } + if (STATE_ENABLE != STATE_LIST_NONE) { + int enable = android.R.attr.state_enabled; + // -enable 代表 state_enable = false + stateList.addState(new int[]{STATE_ENABLE == STATE_LIST_TRUE ? enable : -enable}, getShapeDrawable(enableStrikeWidth, enableStrikeColor, enableSoildColor)); + } + if (STATE_SELECT != STATE_LIST_NONE) { + int select = android.R.attr.state_selected; + stateList.addState(new int[]{STATE_SELECT == STATE_LIST_TRUE ? select : -select}, getShapeDrawable(selectStrikeWidth, selectStrikeColor, selectSoildColor)); + } + if (STATE_CHECK != STATE_LIST_NONE) { + int checked = android.R.attr.state_checked; + stateList.addState(new int[]{STATE_CHECK == STATE_LIST_TRUE ? checked : -checked}, getShapeDrawable(checkStrikeWidth, checkStrikeColor, checkSoildColor)); + } + //默认状态,我们给它设置我空集合 + stateList.addState(new int[]{}, getShapeDrawable(strikeWidth, strikeColor, soildColor)); + return stateList; + } + + private GradientDrawable.Orientation getGradientOrientation() { + GradientDrawable.Orientation orientation = null; + switch (angle) { + case 45: + orientation = GradientDrawable.Orientation.BL_TR; + break; + case 90: + orientation = GradientDrawable.Orientation.BOTTOM_TOP; + break; + case 135: + orientation = GradientDrawable.Orientation.BR_TL; + break; + case 180: + orientation = GradientDrawable.Orientation.RIGHT_LEFT; + break; + case 225: + orientation = GradientDrawable.Orientation.TR_BL; + break; + case 270: + orientation = GradientDrawable.Orientation.TOP_BOTTOM; + break; + case 315: + orientation = GradientDrawable.Orientation.TL_BR; + break; + default: + orientation = GradientDrawable.Orientation.LEFT_RIGHT; + break; + } + return orientation; + } + + private void setGradientDrawable(Context context) { + GradientDrawable gradient = null; + if (startColor != -1 && endColor != -1) { + GradientDrawable.Orientation orientation = getGradientOrientation(); + if (centerColor != -1) { + gradient = new GradientDrawable(orientation, new int[]{startColor, centerColor, endColor}); + } else { + gradient = new GradientDrawable(orientation, new int[]{startColor, endColor}); + } + } + if (gradient != null) { + setBackground(gradient); + } + } + + + + private int[] getDrawablePaddings() { + int[] paddings = new int[4]; + paddings[0] = drawableLeftPadding; + paddings[1] = drawableTopPadding; + paddings[2] = drawableRightPadding; + paddings[3] = drawableBottomPadding; + + if (OtherExtKt.isRtl(getContext())) { + paddings[0] = drawableRightPadding; + paddings[2] = drawableLeftPadding; + } else { + paddings[0] = drawableLeftPadding; + paddings[2] = drawableRightPadding; + } + + return paddings; + } + + + @Override + public void setCompoundDrawablePadding(int pad) { + if (!isInitSuper) { + super.setCompoundDrawablePadding(pad); + return; + } + drawableTopPadding = pad; + drawableBottomPadding = pad; + drawableLeftPadding = pad; + drawableRightPadding = pad; + super.setCompoundDrawablePadding(0); + } + + + private int getDrawablePadding(int index) { + Drawable[] drawables = getCompoundDrawables(); + if (drawables[index] != null) { + return getDrawablePaddings()[index]; + } else { + return 0; + } + } + + @Override + public int getCompoundPaddingLeft() { + return super.getCompoundPaddingLeft() + getDrawablePadding(0); + } + + @Override + public int getCompoundPaddingTop() { + return super.getCompoundPaddingTop() + getDrawablePadding(1); + } + + @Override + public int getCompoundPaddingRight() { + return super.getCompoundPaddingRight() + getDrawablePadding(2); + } + + @Override + public int getCompoundPaddingBottom() { + return super.getCompoundPaddingBottom() + getDrawablePadding(3); + } + + + + /** + * 重写 drawable 设置方法 ,来自定义 各个drawable 宽高 + */ + @Override + public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) { + if (mLeftDrawable != null || mTopDrawable != null || mRightDrawable != null || mBottomDrawable != null) { + if (left != null) { + left.setBounds(0, 0, getDrawableWidth(left, mDrawableLeftWidth), getDrawableHeight(left, mDrawableLeftHeight)); + } + if (top != null) { + top.setBounds(0, 0, getDrawableWidth(top, mDrawableTopWidth), getDrawableHeight(top, mDrawableTopHeight)); + } + if (right != null) { + right.setBounds(0, 0, getDrawableWidth(right, mDrawableRightWidth), getDrawableHeight(right, mDrawableRightHeight)); + } + if (bottom != null) { + bottom.setBounds(0, 0, getDrawableWidth(bottom, mDrawableBottomWidth), getDrawableHeight(bottom, mDrawableBottomHeight)); + } + + if (OtherExtKt.isRtl(getContext())) { + setCompoundDrawables(right, top, left, bottom); + } else { + setCompoundDrawables(left, top, right, bottom); + } + + } else { + super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); + } + } + + + + + private int getDrawableWidth(Drawable drawable, int size) { + return size > 0 ? size : drawable.getIntrinsicWidth(); + } + + private int getDrawableHeight(Drawable drawable, int size) { + return size > 0 ? size : drawable.getIntrinsicHeight(); + } + + + public Drawable getLeftDrawable() { + return mLeftDrawable; + } + + public Drawable getRightDrawable() { + return mRightDrawable; + } + + public Drawable getTopDrawable() { + return mTopDrawable; + } + + public Drawable getBottomDrawable() { + return mBottomDrawable; + } + + //会指顶线 底线 + private void drawLines(Canvas canvas) { + if (mLineHeight > 0) { + if (mLinePaint == null) { + mLinePaint = new Paint(); + mLinePaint.setColor(mLineColor == -1 ? getTextColors().getDefaultColor() : mLineColor); + //抗锯齿 + mLinePaint.setAntiAlias(true); + //防抖动 + mLinePaint.setDither(true); + //设置线条宽度(单位px) + mLinePaint.setStrokeWidth(mLineHeight); + } + + //线绘制的位置 + if (mLineLocation == LINE_TOP) { + canvas.drawLine(0 + mLineMarginLeft, 0, getMeasuredWidth() - mLineMarginRight, 0, mLinePaint); + } else if (mLineLocation == LINE_BOTTOM) { + canvas.drawLine(0 + mLineMarginLeft, getMeasuredHeight(), getMeasuredWidth() - mLineMarginRight, getMeasuredHeight(), mLinePaint); + } else { + canvas.drawLine(0 + mLineMarginLeft, 0, getMeasuredWidth(), 0, mLinePaint); + canvas.drawLine(0 + mLineMarginLeft, getMeasuredHeight(), getMeasuredWidth() - mLineMarginRight, getMeasuredHeight(), mLinePaint); + } + } + } + + + /** + * 简单的设置渐变 + * 颜色示例 : R.color.color_F4F3FB.getColor() + */ + public void setGradientDrawable(int startColor, int centerColor, int endColor,int angle, float radius) { + if (startColor != -1) this.startColor = startColor; + if (centerColor != -1) this.centerColor = centerColor; + if (endColor != -1) this.endColor = endColor; + if (radius != -1f) this.radius = radius; + if (angle != -1) this.angle = angle; + + GradientDrawable gradient = null; + if (this.startColor != -1 && this.endColor != -1) { + GradientDrawable.Orientation orientation = getGradientOrientation(); + if (this.centerColor != -1) { + gradient = new GradientDrawable(orientation, new int[]{this.startColor, this.centerColor, this.endColor}); + } else { + gradient = new GradientDrawable(orientation, new int[]{this.startColor, this.endColor}); + } + } + if (gradient != null && this.radius != -1) { + gradient.setCornerRadius(this.radius); + } + if (gradient != null) { + setBackground(gradient); + } + } + + public void changeGradientColor(int startColor, int centerColor, int endColor) { + if (startColor != -1) this.startColor = startColor; + if (centerColor != -1) this.centerColor = centerColor; + if (endColor != -1) this.endColor = endColor; + initDrawable(getContext()); + } + + /** + * 改变shape的背景颜色 需传入ARGB八位 例如#FFFFFFFF + * + * @param changeColor 颜色值 + */ + public void changeSoildColor(int changeColor) { + this.soildColor = changeColor; + changeDrawableColor(); + } + + /** + * 改变shape的 边框颜色 需传入ARGB八位 例如#FFFFFFFF + * + * @param changeColor 颜色值 + */ + public void changeStrikeColor(int changeColor) { + this.strikeColor = changeColor; + changeDrawableColor(); + } + public void changeStrikeColor(int changeColor,int strikeWidth) { + this.strikeColor = changeColor; + this.strikeWidth = strikeWidth; + changeDrawableColor(); + } + + /** + * 改变shape Type + */ + public void setDrawableType(int drawableType) { + this.drawableType = drawableType; + invalidate(); + } + + + /** + * 设置 mLeftDrawable, mRightDrawable, mTopDrawable, mBottomDrawable; + */ + public void setDrawable(Drawable mLeftDrawable, Drawable mTopDrawable, Drawable mRightDrawable, Drawable mBottomDrawable) { + if (mLeftDrawable != null) + this.mLeftDrawable = mLeftDrawable; + if (mTopDrawable != null) + this.mTopDrawable = mTopDrawable; + if (mRightDrawable != null) + this.mRightDrawable = mRightDrawable; + if (mBottomDrawable != null) + this.mBottomDrawable = mBottomDrawable; + if (this.mLeftDrawable != null || this.mTopDrawable != null || this.mRightDrawable != null || this.mBottomDrawable != null) { + setCompoundDrawablesWithIntrinsicBounds(this.mLeftDrawable, this.mTopDrawable, this.mRightDrawable, this.mBottomDrawable); + } + } + + /** + * 不做空判断,可以用来清空Drawable + * 设置 mLeftDrawable, mRightDrawable, mTopDrawable, mBottomDrawable; + */ + public void setDrawableEmpty(Drawable mLeftDrawable, Drawable mTopDrawable, Drawable mRightDrawable, Drawable mBottomDrawable) { + this.mLeftDrawable = mLeftDrawable; + this.mTopDrawable = mTopDrawable; + this.mRightDrawable = mRightDrawable; + this.mBottomDrawable = mBottomDrawable; + setCompoundDrawablesWithIntrinsicBounds(this.mLeftDrawable, this.mTopDrawable, this.mRightDrawable, this.mBottomDrawable); + } + + /** + * 改变 drawable 宽高 + */ + public void setDrawableSize(int gravity, int width, int height, boolean refresh) { + switch (gravity) { + case Gravity.LEFT: + case Gravity.START: + mDrawableLeftWidth = width; + mDrawableLeftHeight = height; + break; + case Gravity.TOP: + mDrawableTopWidth = width; + mDrawableTopHeight = height; + break; + case Gravity.RIGHT: + case Gravity.END: + mDrawableRightWidth = width; + mDrawableRightHeight = height; + break; + case Gravity.BOTTOM: + mDrawableBottomWidth = width; + mDrawableBottomHeight = height; + break; + } + if (refresh) + setCompoundDrawablesWithIntrinsicBounds(this.mLeftDrawable, this.mTopDrawable, this.mRightDrawable, this.mBottomDrawable); + } + + public int getDrawableTopPadding() { + return drawableTopPadding; + } + + public void setDrawableTopPadding(int drawableTopPadding) { + this.drawableTopPadding = drawableTopPadding; + } + + public int getDrawableBottomPadding() { + return drawableBottomPadding; + } + + public void setDrawableBottomPadding(int drawableBottomPadding) { + this.drawableBottomPadding = drawableBottomPadding; + } + + public int getDrawableLeftPadding() { + return drawableLeftPadding; + } + + public void setDrawableLeftPadding(int drawableLeftPadding) { + this.drawableLeftPadding = drawableLeftPadding; + } + + public int getDrawableRightPadding() { + return drawableRightPadding; + } + + public void setDrawableRightPadding(int drawableRightPadding) { + this.drawableRightPadding = drawableRightPadding; + } + + /** + * 设置一个居中的 Drawable + * //方法是设置 负的 DrawablePadding ,此处以 mLeftDrawable 为例子 + */ + public void setCenterDrawable(Drawable mTopDrawable) { + this.setCompoundDrawablePadding(-mDrawableTopWidth); + setDrawable(null, mTopDrawable,null , null); + } + + + /** + * 设置粗体 + */ + public void setBold(boolean isBold) { + setTypeface(Typeface.defaultFromStyle(isBold ? Typeface.BOLD : Typeface.NORMAL)); + } + + + /** + * 方便属性动画 + * ObjectAnimator.ofInt(mBtnPost, "width", mBtnPost.getHeight()).setDuration(300).start(); + */ + public void setWidth(int width){ + getLayoutParams().width = width; + requestLayout(); + } + + /** + * 渐变角度 + * + * + * + * + * + * + * + * + */ + public void setAngle(int angle) { + this.angle = angle; + } + + public float getRadius() { + return radius; + } + + public int getAngle() { + return angle; + } +} diff --git a/library/src/main/java/com/chwl/library/widget/xlist/FamiliarDefaultItemDecoration.java b/library/src/main/java/com/chwl/library/widget/xlist/FamiliarDefaultItemDecoration.java new file mode 100644 index 0000000..9acd2de --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/xlist/FamiliarDefaultItemDecoration.java @@ -0,0 +1,569 @@ +package com.chwl.library.widget.xlist; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; + +/** + * FamiliarRecyclerView Default ItemDecoration + * Created by iWgang on 15/11/08. + * https://github.com/iwgang/FamiliarRecyclerView + */ +public class FamiliarDefaultItemDecoration extends RecyclerView.ItemDecoration { + private FamiliarRecyclerView mFamiliarRecyclerView; + private Drawable mVerticalDividerDrawable; + private Drawable mHorizontalDividerDrawable; + private int mVerticalDividerDrawableHeight; + private int mHorizontalDividerDrawableHeight; + private int mItemViewBothSidesMargin; + private int mTopPadding; + + private int mLayoutManagerType = FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR; // FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_* + private int mOrientation = OrientationHelper.VERTICAL; // OrientationHelper.VERTICAL or OrientationHelper.HORIZONTAL + private int mGridSpanCount = 0; + private boolean isHeaderDividersEnabled; + private boolean isFooterDividersEnabled; + private boolean isNotShowGridEndDivider; + private boolean isShowTopDivider; + private boolean isShowBottomDivider; + private float mUnDivisibleValue = 0; + private boolean isDivisible = true; + + public FamiliarDefaultItemDecoration(FamiliarRecyclerView familiarRecyclerView, Drawable dividerVertical, Drawable dividerHorizontal, int dividerDrawableSizeVertical, int dividerDrawableSizeHorizontal + , int topPadding) { + this.mFamiliarRecyclerView = familiarRecyclerView; + this.mVerticalDividerDrawable = dividerVertical; + this.mHorizontalDividerDrawable = dividerHorizontal; + this.mVerticalDividerDrawableHeight = dividerDrawableSizeVertical; + this.mHorizontalDividerDrawableHeight = dividerDrawableSizeHorizontal; + this.mTopPadding = topPadding; + initLayoutManagerType(); + } + + private void initLayoutManagerType() { + this.mLayoutManagerType = mFamiliarRecyclerView.getCurLayoutManagerType(); + RecyclerView.LayoutManager layoutManager = mFamiliarRecyclerView.getLayoutManager(); + switch (mLayoutManagerType) { + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR: + LinearLayoutManager curLinearLayoutManager = (LinearLayoutManager)layoutManager; + if (curLinearLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) { + mOrientation = OrientationHelper.HORIZONTAL; + } else { + mOrientation = OrientationHelper.VERTICAL; + } + break; + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID: + GridLayoutManager curGridLayoutManager = (GridLayoutManager)layoutManager; + mGridSpanCount = curGridLayoutManager.getSpanCount(); + if (curGridLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) { + mOrientation = OrientationHelper.HORIZONTAL; + } else { + mOrientation = OrientationHelper.VERTICAL; + } + break; + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID: + StaggeredGridLayoutManager curStaggeredGridLayoutManager = (StaggeredGridLayoutManager)layoutManager; + mGridSpanCount = curStaggeredGridLayoutManager.getSpanCount(); + if (curStaggeredGridLayoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) { + mOrientation = OrientationHelper.HORIZONTAL; + } else { + mOrientation = OrientationHelper.VERTICAL; + } + break; + default: + this.mLayoutManagerType = FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR; + } + + initDivisible(); + } + + private void initDivisible() { + int tempDividerDrawableSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + + if (mGridSpanCount > 0 && tempDividerDrawableSize % mGridSpanCount != 0) { + float t1 = (float)tempDividerDrawableSize / mGridSpanCount; + mUnDivisibleValue = t1 - (int)t1; + isDivisible = false; + } else { + mUnDivisibleValue = 0; + isDivisible = true; + } + } + + public void setVerticalDividerDrawable(Drawable verticalDividerDrawable) { + this.mVerticalDividerDrawable = verticalDividerDrawable; + } + + public void setHorizontalDividerDrawable(Drawable horizontalDividerDrawable) { + this.mHorizontalDividerDrawable = horizontalDividerDrawable; + } + + public void setVerticalDividerDrawableHeight(int verticalDividerDrawableHeight) { + this.mVerticalDividerDrawableHeight = verticalDividerDrawableHeight; + initDivisible(); + } + + public void setHorizontalDividerDrawableHeight(int horizontalDividerDrawableHeight) { + this.mHorizontalDividerDrawableHeight = horizontalDividerDrawableHeight; + initDivisible(); + } + + public void setItemViewBothSidesMargin (int itemViewBothSidesMargin) { + this.mItemViewBothSidesMargin = itemViewBothSidesMargin; + } + + public void setHeaderDividersEnabled(boolean isHeaderDividersEnabled) { + this.isHeaderDividersEnabled = isHeaderDividersEnabled; + } + + public void setFooterDividersEnabled(boolean isFooterDividersEnabled) { + this.isFooterDividersEnabled = isFooterDividersEnabled; + } + + public void setNotShowGridEndDivider(boolean isNotShowGridEndDivider) { + this.isNotShowGridEndDivider = isNotShowGridEndDivider; + } + + public void setShowTopDivider(boolean isShowTopDivider) { + this.isShowTopDivider = isShowTopDivider; + } + + public void setShowBottomDivider(boolean isShowBottomDivider) { + this.isShowBottomDivider = isShowBottomDivider; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (null != mVerticalDividerDrawable || null != mHorizontalDividerDrawable) { + drawDividerDrawable(c, parent); + } + } + + /** + * Draw Divider Drawable + * @param c Canvas + * @param parent RecyclerView + */ + private void drawDividerDrawable(Canvas c, RecyclerView parent) { + int headersCount = 0; + int footerCount = 0; + int itemViewCount; + + FamiliarRecyclerView curFamiliarRecyclerView = null; + if (parent instanceof FamiliarRecyclerView) { + curFamiliarRecyclerView = (FamiliarRecyclerView) parent; + headersCount = curFamiliarRecyclerView.getHeaderViewsCount(); + footerCount = curFamiliarRecyclerView.getFooterViewsCount(); + itemViewCount = curFamiliarRecyclerView.getAdapter().getItemCount() - headersCount - footerCount; + } else { + itemViewCount = parent.getAdapter().getItemCount(); + } + + final int parentLeft = parent.getPaddingLeft(); + final int parentRight = parent.getWidth() - parent.getPaddingRight(); + final int parentTop = parent.getPaddingTop(); + final int parentBottom = parent.getHeight() - parent.getPaddingBottom(); + boolean isGridItemLayoutLastRow, isGridItemLayoutFirstRow, isGridItemLayoutLastColumn; + boolean isGridLayoutLastNum = false; + + for (int i = 0; i < parent.getChildCount(); i++) { + View childView = parent.getChildAt(i); + RecyclerView.LayoutParams childViewParams = (RecyclerView.LayoutParams) childView.getLayoutParams(); + int position = childViewParams.getViewAdapterPosition(); + + // intercept filter + if (isInterceptFilter(position, headersCount, footerCount, itemViewCount)) continue ; + + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR + && (!isHeaderDividersEnabled || headersCount == 0) && position - headersCount == 0) { + continue ; + } + + int traX = (int)(ViewCompat.getTranslationX(childView)); + int traY = (int)(ViewCompat.getTranslationY(childView)); + + switch (mLayoutManagerType) { + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR: + // LinearLayoutManager + if (mOrientation == OrientationHelper.VERTICAL) { + int left = parentLeft; + int right = parentRight; + if (mItemViewBothSidesMargin > 0 && position - headersCount > 0) { + left += mItemViewBothSidesMargin; + right -= mItemViewBothSidesMargin; + } + final int top = childView.getTop() - childViewParams.topMargin - mHorizontalDividerDrawableHeight; + final int bottom = top + mHorizontalDividerDrawableHeight; + mHorizontalDividerDrawable.setBounds(left + traX, top + traY, right + traX, bottom + traY); + mHorizontalDividerDrawable.draw(c); + } else { + int top = parentTop; + int bottom = parentBottom; + if (mItemViewBothSidesMargin > 0 && position - headersCount > 0) { + top += mItemViewBothSidesMargin; + bottom -= mItemViewBothSidesMargin; + } + + final int left = childView.getLeft() - childViewParams.leftMargin - mVerticalDividerDrawableHeight; + final int right = left + mVerticalDividerDrawableHeight; + mVerticalDividerDrawable.setBounds(left + traX, top + traY, right + traX, bottom + traY); + mVerticalDividerDrawable.draw(c); + } + break; + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID: + case FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID: + // GridLayoutManager or StaggeredGridLayoutManager + isGridItemLayoutLastRow = isGridItemLayoutLastRow(position, itemViewCount, headersCount); + isGridItemLayoutLastColumn = isGridItemLayoutLastColumn(position, headersCount, childView); + isGridItemLayoutFirstRow = isGridItemLayoutFirstRow(position, headersCount); + + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && position == (itemViewCount + headersCount - 1)) { + isGridLayoutLastNum = true; + } + + if (mOrientation == OrientationHelper.HORIZONTAL) { + // horizontal draw divider + if (!isNotShowGridEndDivider || (!isGridLayoutLastNum && !isGridItemLayoutLastColumn)) { + int horizontalLeft = childView.getLeft() - childViewParams.leftMargin; + int horizontalTop = childView.getBottom() + childViewParams.bottomMargin; + int horizontalRight = childView.getRight() + childViewParams.rightMargin; + int horizontalBottom = horizontalTop + mHorizontalDividerDrawableHeight; + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID && !isGridItemLayoutLastRow) { + horizontalRight += mVerticalDividerDrawableHeight; + } + mHorizontalDividerDrawable.setBounds(horizontalLeft + traX, horizontalTop + traY, horizontalRight + traX, horizontalBottom + traY); + mHorizontalDividerDrawable.draw(c); + } + + if ((!isHeaderDividersEnabled || headersCount == 0) && isGridItemLayoutFirstRow) { + continue ; + } + + int verticalTop; + int verticalBottom; + if (isGridItemLayoutFirstRow) { + // Only draw the first line of the first grid + if (position - headersCount == 0) { + verticalTop = parent.getTop(); + verticalBottom = parent.getBottom(); + } else { + continue ; + } + } else if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && isGridItemLayoutLastRow) { + // Only draw the last line of the first grid + if (isGridItemLayoutFirstColumn(position, headersCount, childView)) { + verticalTop = parent.getTop() + mItemViewBothSidesMargin; + verticalBottom = parent.getBottom() - mItemViewBothSidesMargin; + } else { + continue ; + } + } else { + verticalTop = childView.getTop() - childViewParams.topMargin; + verticalBottom = childView.getBottom() + childViewParams.bottomMargin; + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && !isGridItemLayoutLastColumn) { + verticalBottom += mHorizontalDividerDrawableHeight; + } + } + + int verticalLeft = childView.getLeft() - childViewParams.leftMargin - mVerticalDividerDrawableHeight; + int verticalRight = verticalLeft + mVerticalDividerDrawableHeight; + + mVerticalDividerDrawable.setBounds(verticalLeft + traX, verticalTop + traY, verticalRight + traX, verticalBottom + traY); + mVerticalDividerDrawable.draw(c); + } else { + // draw vertical divider + if (!isNotShowGridEndDivider || (!isGridItemLayoutLastColumn + && ((mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && !isGridLayoutLastNum) + || mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID))) { + int verticalLeft = childView.getRight() + childViewParams.rightMargin; + int verticalTop = childView.getTop() - childViewParams.topMargin; + int verticalRight = verticalLeft + mVerticalDividerDrawableHeight; + int verticalBottom = childView.getBottom() + childViewParams.bottomMargin; + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID && !isGridItemLayoutLastRow) { + verticalBottom += mHorizontalDividerDrawableHeight; + } + mVerticalDividerDrawable.setBounds(verticalLeft + traX, verticalTop + traY, verticalRight + traX, verticalBottom + traY); + mVerticalDividerDrawable.draw(c); + } + + if ((!isHeaderDividersEnabled || headersCount == 0) && isGridItemLayoutFirstRow) { + continue ; + } + + int horizontalLeft; + int horizontalRight; + + if (isGridItemLayoutFirstRow) { + // Only draw the first line of the first grid + if (position - headersCount == 0) { + horizontalLeft = parent.getLeft(); + horizontalRight = parent.getRight(); + } else { + continue ; + } + } else if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && isGridItemLayoutLastRow) { + // Only draw the last line of the first grid + if (isGridItemLayoutFirstColumn(position, headersCount, childView)) { + horizontalLeft = parent.getLeft() + mItemViewBothSidesMargin; + horizontalRight = parent.getRight() - mItemViewBothSidesMargin; + } else { + continue ; + } + } else { + horizontalLeft = childView.getLeft() - childViewParams.leftMargin; + horizontalRight = childView.getRight() + childViewParams.rightMargin; + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID && !isGridItemLayoutLastColumn) { + horizontalRight += mVerticalDividerDrawableHeight; + } + } + + int horizontalTop = childView.getTop() - childViewParams.topMargin - mHorizontalDividerDrawableHeight; + int horizontalBottom = horizontalTop + mHorizontalDividerDrawableHeight; + mHorizontalDividerDrawable.setBounds(horizontalLeft + traX, horizontalTop + traY, horizontalRight + traX, horizontalBottom + traY); + mHorizontalDividerDrawable.draw(c); + + } + break; + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); + int position = params.getViewAdapterPosition(); + int headersCount; + int footerCount; + int itemViewCount; + FamiliarRecyclerView curFamiliarRecyclerView = null; + if (parent instanceof FamiliarRecyclerView) { + curFamiliarRecyclerView = (FamiliarRecyclerView) parent; + + footerCount = curFamiliarRecyclerView.getFooterViewsCount(); + headersCount = curFamiliarRecyclerView.getHeaderViewsCount(); + itemViewCount = curFamiliarRecyclerView.getAdapter().getItemCount() - headersCount - footerCount; + } else { + headersCount = 0; + footerCount = 0; + itemViewCount = parent.getAdapter().getItemCount(); + } + + // intercept filter + print("headersCount:" + headersCount + ",footerCount:" + footerCount + ",position:" + position + ",itemViewCount:" + itemViewCount); + //如果需要最顶部分割线 + if (position == 0 && headersCount > 0 && isShowTopDivider) { + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(mVerticalDividerDrawableHeight, 0, 0, 0); + } else { + outRect.set(0, mHorizontalDividerDrawableHeight, 0, 0); + } + return; + } + if (isInterceptFilter(position, headersCount, footerCount, itemViewCount)) return; + + // set headView or footerVie + if (isHeadViewPos(headersCount, position)) { + // head + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(mVerticalDividerDrawableHeight, 0, 0, 0); + } else { + outRect.set(0, mHorizontalDividerDrawableHeight, 0, 0); + } + return ; + } else if (isFooterViewPos(headersCount, footerCount, itemViewCount, position)) { + // footer + print(ResUtil.getString(R.string.widget_xlist_familiardefaultitemdecoration_01)); + int leftOrTopSize; + if (isFooterDividersEnabled) { + leftOrTopSize = mOrientation == OrientationHelper.HORIZONTAL ? + mVerticalDividerDrawableHeight : mHorizontalDividerDrawableHeight; + } else { + leftOrTopSize = 0; + } + int bottomDividerHeight; + if (isShowBottomDivider && position == itemViewCount + headersCount + footerCount - 1) { + bottomDividerHeight = mOrientation == OrientationHelper.HORIZONTAL ? + mVerticalDividerDrawableHeight : mHorizontalDividerDrawableHeight; + } else { + bottomDividerHeight = 0; + } + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(leftOrTopSize, 0, bottomDividerHeight, 0); + } else { + outRect.set(0, leftOrTopSize, 0, bottomDividerHeight); + } + return ; + } + + // set itemView + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID || mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID) { + processGridOffsets(outRect, position, itemViewCount, headersCount, footerCount, view); + } else { + int topOrLeftSize; + if (isShowTopDivider && headersCount == 0 && position == 0) { + topOrLeftSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + } else if ((!isHeaderDividersEnabled || headersCount == 0) && position - headersCount == 0) { + topOrLeftSize = 0; + } else { + topOrLeftSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + } + + int bottomOrRightSize; + if (isShowBottomDivider && footerCount == 0 && position == itemViewCount + headersCount - 1) { + bottomOrRightSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + } else { + bottomOrRightSize = 0; + } + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(topOrLeftSize, mItemViewBothSidesMargin, bottomOrRightSize, mItemViewBothSidesMargin); + } else { + outRect.set(mItemViewBothSidesMargin, topOrLeftSize, mItemViewBothSidesMargin, bottomOrRightSize); + } + } + } + + private void print(String text) { + + } + + private void processGridOffsets(Rect outRect, int position, int itemViewCount, int headersCount, int footerCount, View view) { + int curGridNum; + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID) { + curGridNum = (position - headersCount) % mGridSpanCount; + } else { + curGridNum = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); + } + + int leftOrTopOffset, rightOrBottomOffset; + float mDividerLeftBaseOffset, mDividerRightBaseOffset; + + final int tempDividerDrawableSize = mOrientation == OrientationHelper.HORIZONTAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + if (mItemViewBothSidesMargin > 0) { + // item view left and right margin + mDividerLeftBaseOffset = (float)tempDividerDrawableSize / mGridSpanCount * curGridNum - mItemViewBothSidesMargin * 2 / mGridSpanCount * curGridNum + mItemViewBothSidesMargin; + mDividerRightBaseOffset = (float)tempDividerDrawableSize / mGridSpanCount * (mGridSpanCount - (curGridNum + 1)) + mItemViewBothSidesMargin * 2 / mGridSpanCount * (curGridNum + 1) - mItemViewBothSidesMargin; + } else { + mDividerLeftBaseOffset = (float)tempDividerDrawableSize / mGridSpanCount * curGridNum; + mDividerRightBaseOffset = (float)tempDividerDrawableSize / mGridSpanCount * (mGridSpanCount - (curGridNum + 1)); + } + + if (!isDivisible && mUnDivisibleValue > 0) { + leftOrTopOffset = Math.round(mDividerLeftBaseOffset - mUnDivisibleValue * (curGridNum + 1)); + rightOrBottomOffset = Math.round(mDividerRightBaseOffset + mUnDivisibleValue * (curGridNum + 1)); + } else { + leftOrTopOffset = (int)mDividerLeftBaseOffset; + rightOrBottomOffset = (int)mDividerRightBaseOffset; + } + + int topOrLeftSize; + boolean isFirstRow = isGridItemLayoutFirstRow(position, headersCount); + if (isFirstRow && ((headersCount > 0 && !isHeaderDividersEnabled) || (headersCount == 0 && !isShowTopDivider))) { + topOrLeftSize = 0; + } else { + topOrLeftSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + } + + //是否设置最底部分割线 + int bottomOrRightSize; + boolean isLastRow = isGridItemLayoutLastRow(position, itemViewCount, headersCount); + if (isShowBottomDivider && isLastRow && footerCount == 0) { + bottomOrRightSize = mOrientation == OrientationHelper.VERTICAL ? mHorizontalDividerDrawableHeight : mVerticalDividerDrawableHeight; + } else { + bottomOrRightSize = 0; + } + + + if (isGridItemLayoutLastColumn(position, headersCount, view)) { + // last Column + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(topOrLeftSize, leftOrTopOffset, bottomOrRightSize, mItemViewBothSidesMargin); + } else { + outRect.set(leftOrTopOffset, topOrLeftSize, mItemViewBothSidesMargin, bottomOrRightSize); + } + } else if (isGridItemLayoutFirstColumn(position, headersCount, view)) { + // first column + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(topOrLeftSize, mItemViewBothSidesMargin, bottomOrRightSize, rightOrBottomOffset); + } else { + outRect.set(mItemViewBothSidesMargin, topOrLeftSize, rightOrBottomOffset, bottomOrRightSize); + } + } else { + // middle column + if (mOrientation == OrientationHelper.HORIZONTAL) { + outRect.set(topOrLeftSize, leftOrTopOffset, bottomOrRightSize, rightOrBottomOffset); + } else { + outRect.set(leftOrTopOffset, topOrLeftSize, rightOrBottomOffset, bottomOrRightSize); + } + } + } + + private boolean isHeadViewPos(int headersCount, int position) { + return isHeaderDividersEnabled && headersCount > 0 && position < headersCount; + } + + private boolean isFooterViewPos(int headersCount, int footerCount, int itemViewCount, int position) { +// if (isShowBottomDivider && footerCount > 0 && position >= itemViewCount + headersCount) { +// return true; +// } +// return isFooterDividersEnabled && footerCount > 0 && position >= itemViewCount + headersCount; + return footerCount > 0 && position >= itemViewCount + headersCount; + } + + private boolean isInterceptFilter(int position, int headersCount, int footerCount, int itemViewCount) { + if (isHeaderDividersEnabled && headersCount > 0 && position == 0) return true; + + if (!isHeaderDividersEnabled && position < headersCount) return true; + + if (isShowBottomDivider && footerCount > 0 && position >= itemViewCount + headersCount) return false; + + if ((!isFooterDividersEnabled || footerCount == 0) && position >= itemViewCount + headersCount) return true; + + return false; + } + + private boolean isGridItemLayoutLastRow(int position, int itemViewCount, int headersCount) { + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_LINEAR) return false; + + return Math.ceil((float)itemViewCount / mGridSpanCount) == Math.ceil((float)(position - headersCount + 1) / mGridSpanCount); + } + + private boolean isGridItemLayoutFirstRow(int position, int headersCount) { + return position - headersCount < mGridSpanCount; + } + + private boolean isGridItemLayoutLastColumn(int position, int headersCount, View view) { + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID) { + if ((position + 1 - headersCount) % mGridSpanCount == 0) { + return true; + } + } else if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID) { + int spanIndex = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); + if (spanIndex == mGridSpanCount-1) return true; + } + + return false; + } + + private boolean isGridItemLayoutFirstColumn(int position, int headersCount, View view) { + if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_GRID) { + return (position + 1 - headersCount) % mGridSpanCount == 1; + } else if (mLayoutManagerType == FamiliarRecyclerView.LAYOUT_MANAGER_TYPE_STAGGERED_GRID) { + int spanIndex = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); + if (spanIndex == 0) return true; + } + + return false; + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/chwl/library/widget/xlist/FamiliarRecyclerView.java b/library/src/main/java/com/chwl/library/widget/xlist/FamiliarRecyclerView.java new file mode 100644 index 0000000..229a23b --- /dev/null +++ b/library/src/main/java/com/chwl/library/widget/xlist/FamiliarRecyclerView.java @@ -0,0 +1,503 @@ +package com.chwl.library.widget.xlist; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.chwl.library.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * FamiliarRecyclerView + * Created by iWgang on 15/10/31. + * https://github.com/iwgang/FamiliarRecyclerView + */ +public class FamiliarRecyclerView extends RecyclerView { + public static final int LAYOUT_MANAGER_TYPE_LINEAR = 0; + public static final int LAYOUT_MANAGER_TYPE_GRID = 1; + public static final int LAYOUT_MANAGER_TYPE_STAGGERED_GRID = 2; + + private static final int DEF_LAYOUT_MANAGER_TYPE = LAYOUT_MANAGER_TYPE_LINEAR; + private static final int DEF_GRID_SPAN_COUNT = 2; + private static final int DEF_LAYOUT_MANAGER_ORIENTATION = OrientationHelper.VERTICAL; + private static final int DEF_DIVIDER_HEIGHT = 30; + + private List mHeaderView = new ArrayList(); + private List mFooterView = new ArrayList(); + private GridLayoutManager mCurGridLayoutManager; + private FamiliarDefaultItemDecoration mFamiliarDefaultItemDecoration; + + private Drawable mVerticalDivider; + private Drawable mHorizontalDivider; + private int mTopPadding; + private int mVerticalDividerHeight; + private int mHorizontalDividerHeight; + private int mItemViewBothSidesMargin; + private boolean isShowTopDivider = false; + /**是否显示最顶部分割线*/ + private boolean isShowbottomDivider = false; + private boolean isHeaderDividersEnabled = false; + private boolean isFooterDividersEnabled = false; + private boolean isDefaultItemDecoration = true; + private boolean isKeepShowHeadOrFooter = false; + private boolean isNotShowGridEndDivider = false; + + private int mLayoutManagerType; + private Drawable mDefAllDivider; + private int mDefAllDividerHeight; + private boolean needInitAddItemDescration = false; + private boolean hasShowEmptyView = false; + private Drawable mLoadMoreColor; + /**底部预加载数量*/ + private int preSize = 10; + + public int getPreSize() { + return preSize; + } + + public void setPreSize(int preSize) { + this.preSize = preSize; + } + + public FamiliarRecyclerView(Context context) { + this(context, null); + } + + public FamiliarRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FamiliarRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FamiliarRecyclerView); + mDefAllDivider = ta.getDrawable(R.styleable.FamiliarRecyclerView_frv_divider); + mDefAllDividerHeight = (int)ta.getDimension(R.styleable.FamiliarRecyclerView_frv_dividerHeight, -1); + mVerticalDivider = ta.getDrawable(R.styleable.FamiliarRecyclerView_frv_dividerVertical); + mHorizontalDivider = ta.getDrawable(R.styleable.FamiliarRecyclerView_frv_dividerHorizontal); + mTopPadding = (int) ta.getDimension(R.styleable.FamiliarRecyclerView_frv_topPadding, 0); + mVerticalDividerHeight = (int)ta.getDimension(R.styleable.FamiliarRecyclerView_frv_dividerVerticalHeight, -1); + mHorizontalDividerHeight = (int)ta.getDimension(R.styleable.FamiliarRecyclerView_frv_dividerHorizontalHeight, -1); + mItemViewBothSidesMargin = (int)ta.getDimension(R.styleable.FamiliarRecyclerView_frv_itemViewBothSidesMargin, 0); + isKeepShowHeadOrFooter = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_isEmptyViewKeepShowHeadOrFooter, false); + isHeaderDividersEnabled = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_headerDividersEnabled, false); + isShowTopDivider = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_isShowTopDivider, false); + isShowbottomDivider = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_isShowBottomDivider, false); + isFooterDividersEnabled = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_footerDividersEnabled, false); + isNotShowGridEndDivider = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_isNotShowGridEndDivider, false); + mLoadMoreColor = ta.getDrawable(R.styleable.FamiliarRecyclerView_frv_loadmore_color); + if (ta.hasValue(R.styleable.FamiliarRecyclerView_frv_layoutManager)) { + int layoutManagerType = ta.getInt(R.styleable.FamiliarRecyclerView_frv_layoutManager, DEF_LAYOUT_MANAGER_TYPE); + int layoutManagerOrientation = ta.getInt(R.styleable.FamiliarRecyclerView_frv_layoutManagerOrientation, DEF_LAYOUT_MANAGER_ORIENTATION); + boolean isReverseLayout = ta.getBoolean(R.styleable.FamiliarRecyclerView_frv_isReverseLayout, false); + int gridSpanCount = ta.getInt(R.styleable.FamiliarRecyclerView_frv_spanCount, DEF_GRID_SPAN_COUNT); + + switch (layoutManagerType) { + case LAYOUT_MANAGER_TYPE_LINEAR: + setLayoutManager(new LinearLayoutManager(context, layoutManagerOrientation, isReverseLayout)); + break; + case LAYOUT_MANAGER_TYPE_GRID: + setLayoutManager(new GridLayoutManager(context, gridSpanCount, layoutManagerOrientation, isReverseLayout)); + break; + case LAYOUT_MANAGER_TYPE_STAGGERED_GRID: + setLayoutManager(new StaggeredGridLayoutManager(gridSpanCount, layoutManagerOrientation)); + break; + } + } + ta.recycle(); + } + + private void processDefDivider(boolean isLinearLayoutManager, int layoutManagerOrientation) { + if (!isDefaultItemDecoration) return ; + + if ((null == mVerticalDivider || null == mHorizontalDivider) && null != mDefAllDivider) { + if (isLinearLayoutManager) { + if (layoutManagerOrientation == OrientationHelper.VERTICAL && null == mHorizontalDivider) { + mHorizontalDivider = mDefAllDivider; + } else if (layoutManagerOrientation == OrientationHelper.HORIZONTAL && null == mVerticalDivider) { + mVerticalDivider = mDefAllDivider; + } + } else { + if (null == mVerticalDivider) { + mVerticalDivider = mDefAllDivider; + } + + if (null == mHorizontalDivider) { + mHorizontalDivider = mDefAllDivider; + } + } + } + + if (mVerticalDividerHeight > 0 && mHorizontalDividerHeight > 0) return ; + + if (mDefAllDividerHeight > 0) { + if (isLinearLayoutManager) { + if (layoutManagerOrientation == OrientationHelper.VERTICAL && mHorizontalDividerHeight <= 0) { + mHorizontalDividerHeight = mDefAllDividerHeight; + } else if(layoutManagerOrientation == OrientationHelper.HORIZONTAL && mVerticalDividerHeight <= 0) { + mVerticalDividerHeight = mDefAllDividerHeight; + } + } else { + if (mVerticalDividerHeight <= 0) { + mVerticalDividerHeight = mDefAllDividerHeight; + } + + if (mHorizontalDividerHeight <= 0) { + mHorizontalDividerHeight = mDefAllDividerHeight; + } + } + } else { + if (isLinearLayoutManager) { + if (layoutManagerOrientation == OrientationHelper.VERTICAL && mHorizontalDividerHeight <= 0) { + if (null != mHorizontalDivider) { + if (mHorizontalDivider.getIntrinsicHeight() > 0) { + mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight(); + } else { + mHorizontalDividerHeight = DEF_DIVIDER_HEIGHT; + } + } + } else if(layoutManagerOrientation == OrientationHelper.HORIZONTAL && mVerticalDividerHeight <= 0) { + if (null != mVerticalDivider) { + if (mVerticalDivider.getIntrinsicHeight() > 0) { + mVerticalDividerHeight = mVerticalDivider.getIntrinsicHeight(); + } else { + mVerticalDividerHeight = DEF_DIVIDER_HEIGHT; + } + } + } + } else { + if (mVerticalDividerHeight <= 0 && null != mVerticalDivider) { + if (mVerticalDivider.getIntrinsicHeight() > 0) { + mVerticalDividerHeight = mVerticalDivider.getIntrinsicHeight(); + } else { + mVerticalDividerHeight = DEF_DIVIDER_HEIGHT; + } + } + + if (mHorizontalDividerHeight <= 0 && null != mHorizontalDivider) { + if (mHorizontalDivider.getIntrinsicHeight() > 0) { + mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight(); + } else { + mHorizontalDividerHeight = DEF_DIVIDER_HEIGHT; + } + } + } + } + } + + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + + if (null == layout) return ; + + if (layout instanceof GridLayoutManager) { + mCurGridLayoutManager = ((GridLayoutManager) layout); + mCurGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + if (position < getHeaderViewsCount() || position >= getAdapter().getItemCount() + getHeaderViewsCount()) { + // header or footer span + return mCurGridLayoutManager.getSpanCount(); + } else { + // default item span + return 1; + } + } + }); + + mLayoutManagerType = LAYOUT_MANAGER_TYPE_GRID; + processDefDivider(false, mCurGridLayoutManager.getOrientation()); + initDefaultItemDecoration(); + } else if (layout instanceof StaggeredGridLayoutManager) { + mLayoutManagerType = LAYOUT_MANAGER_TYPE_STAGGERED_GRID; + processDefDivider(false, ((StaggeredGridLayoutManager) layout).getOrientation()); + initDefaultItemDecoration(); + } else if (layout instanceof LinearLayoutManager) { + mLayoutManagerType = LAYOUT_MANAGER_TYPE_LINEAR; + processDefDivider(true, ((LinearLayoutManager)layout).getOrientation()); + initDefaultItemDecoration(); + } + } + + @Override + public void addItemDecoration(ItemDecoration decor) { + if (null == decor) return ; + + // remove default ItemDecoration + if (null != mFamiliarDefaultItemDecoration) { + removeItemDecoration(mFamiliarDefaultItemDecoration); + mFamiliarDefaultItemDecoration = null; + } + + isDefaultItemDecoration = false; + + super.addItemDecoration(decor); + } + + private void initDefaultItemDecoration() { + if (!isDefaultItemDecoration) return ; + + if (null != mFamiliarDefaultItemDecoration) { + super.removeItemDecoration(mFamiliarDefaultItemDecoration); + mFamiliarDefaultItemDecoration = null; + } + + mFamiliarDefaultItemDecoration = new FamiliarDefaultItemDecoration(this, mVerticalDivider, + mHorizontalDivider, mVerticalDividerHeight, mHorizontalDividerHeight, mTopPadding); + mFamiliarDefaultItemDecoration.setItemViewBothSidesMargin(mItemViewBothSidesMargin); + mFamiliarDefaultItemDecoration.setHeaderDividersEnabled(isHeaderDividersEnabled); + mFamiliarDefaultItemDecoration.setFooterDividersEnabled(isFooterDividersEnabled); + mFamiliarDefaultItemDecoration.setNotShowGridEndDivider(isNotShowGridEndDivider); + mFamiliarDefaultItemDecoration.setShowTopDivider(isShowTopDivider); + mFamiliarDefaultItemDecoration.setShowBottomDivider(isShowbottomDivider); + + if (null != getAdapter()) { + needInitAddItemDescration = false; + super.addItemDecoration(mFamiliarDefaultItemDecoration); + } else { + needInitAddItemDescration = true; + } + } + + + public boolean isKeepShowHeadOrFooter() { + return isKeepShowHeadOrFooter; + } + + public void setDivider(int height, Drawable divider) { + if (!isDefaultItemDecoration || height <= 0) return ; + + this.mVerticalDividerHeight = height; + this.mHorizontalDividerHeight = height; + + if (this.mVerticalDivider != divider) { + this.mVerticalDivider = divider; + } + + if (this.mHorizontalDivider != divider) { + this.mHorizontalDivider = divider; + } + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawableHeight(mVerticalDividerHeight); + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawableHeight(mHorizontalDividerHeight); + + mFamiliarDefaultItemDecoration.setVerticalDividerDrawable(mVerticalDivider); + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawable(mHorizontalDivider); + } + } + + public void setDivider(Drawable divider) { + if (!isDefaultItemDecoration || (mVerticalDividerHeight <= 0 && mHorizontalDividerHeight <= 0)) return ; + + if (this.mVerticalDivider != divider) { + this.mVerticalDivider = divider; + } + + if (this.mHorizontalDivider != divider) { + this.mHorizontalDivider = divider; + } + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawable(mVerticalDivider); + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawable(mHorizontalDivider); + } + } + + public void setDivider(Drawable dividerVertical, Drawable dividerHorizontal) { + if (!isDefaultItemDecoration || (mVerticalDividerHeight <= 0 && mHorizontalDividerHeight <= 0)) return ; + + if (this.mVerticalDivider != dividerVertical) { + this.mVerticalDivider = dividerVertical; + } + + if (this.mHorizontalDivider != dividerHorizontal) { + this.mHorizontalDivider = dividerHorizontal; + } + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawable(mVerticalDivider); + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawable(mHorizontalDivider); + } + } + + public void setDividerVertical(Drawable dividerVertical) { + if (!isDefaultItemDecoration || mVerticalDividerHeight <= 0) return ; + + if (this.mVerticalDivider != dividerVertical) { + this.mVerticalDivider = dividerVertical; + } + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawable(mVerticalDivider); + } + } + + public void setDividerHorizontal(Drawable dividerHorizontal) { + if (!isDefaultItemDecoration || mHorizontalDividerHeight <= 0) return ; + + if (this.mHorizontalDivider != dividerHorizontal) { + this.mHorizontalDivider = dividerHorizontal; + } + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawable(mHorizontalDivider); + } + } + + public void setDividerHeight(int height) { + if (!isDefaultItemDecoration) return ; + + this.mVerticalDividerHeight = height; + this.mHorizontalDividerHeight = height; + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawableHeight(mVerticalDividerHeight); + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawableHeight(mHorizontalDividerHeight); + } + } + + public void setDividerVerticalHeight(int height) { + if (!isDefaultItemDecoration) return ; + + this.mVerticalDividerHeight = height; + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setVerticalDividerDrawableHeight(mVerticalDividerHeight); + } + } + + public void setDividerHorizontalHeight(int height) { + if (!isDefaultItemDecoration) return ; + + this.mHorizontalDividerHeight = height; + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setHorizontalDividerDrawableHeight(mHorizontalDividerHeight); + } + } + + public void setItemViewBothSidesMargin(int bothSidesMargin) { + if (!isDefaultItemDecoration) return ; + + this.mItemViewBothSidesMargin = bothSidesMargin; + + if (null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setItemViewBothSidesMargin(mItemViewBothSidesMargin); + } + } + + + public int getHeaderViewsCount() { + return mHeaderView.size(); + } + + public int getFooterViewsCount() { + return mFooterView.size(); + } + + public int getFirstVisiblePosition() { + LayoutManager layoutManager = getLayoutManager(); + + if (null == layoutManager) return 0; + + int ret = -1; + + switch (mLayoutManagerType) { + case LAYOUT_MANAGER_TYPE_LINEAR: + ret = ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition() - getHeaderViewsCount(); + break; + case LAYOUT_MANAGER_TYPE_GRID: + ret = ((GridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition() - getHeaderViewsCount(); + break; + case LAYOUT_MANAGER_TYPE_STAGGERED_GRID: + StaggeredGridLayoutManager tempStaggeredGridLayoutManager = (StaggeredGridLayoutManager)layoutManager; + int[] firstVisibleItemPositions = new int[tempStaggeredGridLayoutManager.getSpanCount()]; + tempStaggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(firstVisibleItemPositions); + ret = firstVisibleItemPositions[0] - getHeaderViewsCount(); + break; + } + + return ret < 0 ? 0 : ret; + } + + public int getLastVisiblePosition() { + LayoutManager layoutManager = getLayoutManager(); + if (null == layoutManager) return -1; + + int curItemCount = (null != getAdapter() ? getAdapter().getItemCount() - 1 : 0); + int ret = -1; + + switch (mLayoutManagerType) { + case LAYOUT_MANAGER_TYPE_LINEAR: + ret = ((LinearLayoutManager)layoutManager).findLastCompletelyVisibleItemPosition() - getHeaderViewsCount(); + if (ret > curItemCount) { + ret -= getFooterViewsCount(); + } + break; + case LAYOUT_MANAGER_TYPE_GRID: + ret = ((GridLayoutManager)layoutManager).findLastCompletelyVisibleItemPosition() - getHeaderViewsCount(); + if (ret > curItemCount) { + ret -= getFooterViewsCount(); + } + break; + case LAYOUT_MANAGER_TYPE_STAGGERED_GRID: + StaggeredGridLayoutManager tempStaggeredGridLayoutManager = (StaggeredGridLayoutManager)layoutManager; + int[] lastVisibleItemPositions = new int[tempStaggeredGridLayoutManager.getSpanCount()]; + tempStaggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(lastVisibleItemPositions); + if (lastVisibleItemPositions.length > 0) { + int maxPos = lastVisibleItemPositions[0]; + for (int curPos : lastVisibleItemPositions) { + if (curPos > maxPos) maxPos = curPos; + } + ret = maxPos - getHeaderViewsCount(); + if (ret > curItemCount) { + ret -= getFooterViewsCount(); + } + } + break; + } + + return ret < 0 ? (null != getAdapter() ? getAdapter().getItemCount() - 1 : 0) : ret; + } + + public void setHeaderDividersEnabled(boolean isHeaderDividersEnabled) { + this.isHeaderDividersEnabled = isHeaderDividersEnabled; + if (isDefaultItemDecoration && null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setHeaderDividersEnabled(isHeaderDividersEnabled); + } + } + + public void setFooterDividersEnabled(boolean isFooterDividersEnabled) { + this.isFooterDividersEnabled = isFooterDividersEnabled; + if (isDefaultItemDecoration && null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setFooterDividersEnabled(isFooterDividersEnabled); + } + } + + public void setNotShowGridEndDivider(boolean isNotShowGridEndDivider) { + this.isNotShowGridEndDivider = isNotShowGridEndDivider; + if (isDefaultItemDecoration && null != mFamiliarDefaultItemDecoration) { + mFamiliarDefaultItemDecoration.setNotShowGridEndDivider(isNotShowGridEndDivider); + } + } + + public int getCurLayoutManagerType() { + return mLayoutManagerType; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt b/library/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt new file mode 100644 index 0000000..f600a33 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt @@ -0,0 +1,9 @@ +package com.opensource.svgaplayer + +/** + * Created by miaojun on 2019/6/21. + * mail:1290846731@qq.com + */ +interface IClickAreaListener{ + fun onResponseArea(key : String,x0 : Int, y0 : Int, x1 : Int, y1 : Int) +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGACache.kt b/library/src/main/java/com/opensource/svgaplayer/SVGACache.kt new file mode 100644 index 0000000..8e7b503 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGACache.kt @@ -0,0 +1,119 @@ +package com.opensource.svgaplayer + +import android.content.Context +import com.opensource.svgaplayer.utils.log.LogUtils +import java.io.File +import java.net.URL +import java.security.MessageDigest + +/** + * SVGA 缓存管理 + */ +object SVGACache { + enum class Type { + DEFAULT, + FILE + } + + private const val TAG = "SVGACache" + private var type: Type = Type.DEFAULT + private var cacheDir: String = "/" + get() { + if (field != "/") { + val dir = File(field) + if (!dir.exists()) { + dir.mkdirs() + } + } + return field + } + + + fun onCreate(context: Context?) { + onCreate(context, Type.DEFAULT) + } + + fun onCreate(context: Context?, type: Type) { + if (isInitialized()) return + context ?: return + cacheDir = "${context.cacheDir.absolutePath}/svga/" + File(cacheDir).takeIf { !it.exists() }?.mkdirs() + this.type = type + } + + /** + * 清理缓存 + */ + fun clearCache() { + if (!isInitialized()) { + LogUtils.error(TAG, "SVGACache is not init!") + return + } + SVGAParser.threadPoolExecutor.execute { + clearDir(cacheDir) + LogUtils.info(TAG, "Clear svga cache done!") + } + } + + // 清除目录下的所有文件 + internal fun clearDir(path: String) { + try { + val dir = File(path) + dir.takeIf { it.exists() }?.let { parentDir -> + parentDir.listFiles()?.forEach { file -> + if (!file.exists()) { + return@forEach + } + if (file.isDirectory) { + clearDir(file.absolutePath) + } + file.delete() + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "Clear svga cache path: $path fail", e) + } + } + + fun isInitialized(): Boolean { + return "/" != cacheDir && File(cacheDir).exists() + } + + fun isDefaultCache(): Boolean = type == Type.DEFAULT + + fun isCached(cacheKey: String): Boolean { + return if (isDefaultCache()) { + buildCacheDir(cacheKey) + } else { + buildSvgaFile( + cacheKey + ) + }.exists() + } + + fun buildCacheKey(str: String): String { + val messageDigest = MessageDigest.getInstance("MD5") + messageDigest.update(str.toByteArray(charset("UTF-8"))) + val digest = messageDigest.digest() + var sb = "" + for (b in digest) { + sb += String.format("%02x", b) + } + return sb + } + + fun buildCacheKey(url: URL): String = buildCacheKey(url.toString()) + + fun buildCacheDir(cacheKey: String): File { + return File("$cacheDir$cacheKey/") + } + + fun buildSvgaFile(cacheKey: String): File { + return File("$cacheDir$cacheKey.svga") + } + + fun buildAudioFile(audio: String): File { + return File("$cacheDir$audio.mp3") + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGACallback.kt b/library/src/main/java/com/opensource/svgaplayer/SVGACallback.kt new file mode 100644 index 0000000..0e5a1c3 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGACallback.kt @@ -0,0 +1,13 @@ +package com.opensource.svgaplayer + +/** + * Created by cuiminghui on 2017/3/30. + */ +interface SVGACallback { + + fun onPause() + fun onFinished() + fun onRepeat() + fun onStep(frame: Int, percentage: Double) + +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt b/library/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt new file mode 100644 index 0000000..ccdb6e4 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt @@ -0,0 +1,9 @@ +package com.opensource.svgaplayer + +/** + * Created by miaojun on 2019/6/21. + * mail:1290846731@qq.com + */ +interface SVGAClickAreaListener{ + fun onClick(clickKey : String) +} diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt b/library/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt new file mode 100644 index 0000000..1ba04b3 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt @@ -0,0 +1,106 @@ +package com.opensource.svgaplayer + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable +import android.widget.ImageView +import com.opensource.svgaplayer.drawer.SVGACanvasDrawer + +class SVGADrawable(val videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity): Drawable() { + + constructor(videoItem: SVGAVideoEntity): this(videoItem, SVGADynamicEntity()) + + var cleared = true + internal set (value) { + if (field == value) { + return + } + field = value + invalidateSelf() + } + + var currentFrame = 0 + internal set (value) { + if (field == value) { + return + } + field = value + invalidateSelf() + } + + var scaleType: ImageView.ScaleType = ImageView.ScaleType.MATRIX + + private val drawer = SVGACanvasDrawer(videoItem, dynamicItem) + + override fun draw(canvas: Canvas) { + if (cleared) { + return + } + canvas?.let { + drawer.drawFrame(it,currentFrame, scaleType) + } + } + + override fun setAlpha(alpha: Int) { + + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSPARENT + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + + } + + fun resume() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.resume(it) + }else{ + videoItem.soundPool?.resume(it) + } + } + } + } + + fun pause() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.pause(it) + }else{ + videoItem.soundPool?.pause(it) + } + } + } + } + + fun stop() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.stop(it) + }else{ + videoItem.soundPool?.stop(it) + } + } + } + } + + fun clear() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.stop(it) + }else{ + videoItem.soundPool?.stop(it) + } + } + audio.playID = null + } + videoItem.clear() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt b/library/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt new file mode 100644 index 0000000..f578c47 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt @@ -0,0 +1,153 @@ +package com.opensource.svgaplayer + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.text.BoringLayout +import android.text.StaticLayout +import android.text.TextPaint +import java.net.HttpURLConnection +import java.net.URL + +/** + * Created by cuiminghui on 2017/3/30. + */ +class SVGADynamicEntity { + + internal var dynamicHidden: HashMap = hashMapOf() + + internal var dynamicImage: HashMap = hashMapOf() + + internal var dynamicText: HashMap = hashMapOf() + + internal var dynamicTextPaint: HashMap = hashMapOf() + + internal var dynamicStaticLayoutText: HashMap = hashMapOf() + + internal var dynamicBoringLayoutText: HashMap = hashMapOf() + + internal var dynamicDrawer: HashMap Boolean> = hashMapOf() + + //点击事件回调map + internal var mClickMap : HashMap = hashMapOf() + internal var dynamicIClickArea: HashMap = hashMapOf() + + internal var dynamicDrawerSized: HashMap Boolean> = hashMapOf() + + + internal var isTextDirty = false + + fun setHidden(value: Boolean, forKey: String) { + this.dynamicHidden.put(forKey, value) + } + + fun setDynamicImage(bitmap: Bitmap, forKey: String) { + this.dynamicImage.put(forKey, bitmap) + } + + fun setDynamicImage(url: String, forKey: String) { + val handler = android.os.Handler() + SVGAParser.threadPoolExecutor.execute { + (URL(url).openConnection() as? HttpURLConnection)?.let { + try { + it.connectTimeout = 20 * 1000 + it.requestMethod = "GET" + it.connect() + it.inputStream.use { stream -> + BitmapFactory.decodeStream(stream)?.let { + handler.post { setDynamicImage(it, forKey) } + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + it.disconnect() + } catch (disconnectException: Throwable) { + // ignored here + } + } + } + } + } + + fun setDynamicText(text: String, textPaint: TextPaint, forKey: String) { + this.isTextDirty = true + this.dynamicText.put(forKey, text) + this.dynamicTextPaint.put(forKey, textPaint) + } + + fun setDynamicText(layoutText: StaticLayout, forKey: String) { + this.isTextDirty = true + this.dynamicStaticLayoutText.put(forKey, layoutText) + } + + fun setDynamicText(layoutText: BoringLayout, forKey: String) { + this.isTextDirty = true + BoringLayout.isBoring(layoutText.text,layoutText.paint)?.let { + this.dynamicBoringLayoutText.put(forKey,layoutText) + } + } + + fun setDynamicDrawer(drawer: (canvas: Canvas, frameIndex: Int) -> Boolean, forKey: String) { + this.dynamicDrawer.put(forKey, drawer) + } + + fun setClickArea(clickKey: List) { + for(itemKey in clickKey){ + dynamicIClickArea.put(itemKey,object : IClickAreaListener { + override fun onResponseArea(key: String, x0: Int, y0: Int, x1: Int, y1: Int) { + mClickMap.let { + if(it.get(key) == null){ + it.put(key, intArrayOf(x0,y0,x1,y1)) + }else{ + it.get(key)?.let { + it[0] = x0 + it[1] = y0 + it[2] = x1 + it[3] = y1 + } + } + } + } + }) + } + } + + fun setClickArea(clickKey: String) { + dynamicIClickArea.put(clickKey, object : IClickAreaListener { + override fun onResponseArea(key: String, x0: Int, y0: Int, x1: Int, y1: Int) { + mClickMap.let { + if (it.get(key) == null) { + it.put(key, intArrayOf(x0, y0, x1, y1)) + } else { + it.get(key)?.let { + it[0] = x0 + it[1] = y0 + it[2] = x1 + it[3] = y1 + } + } + } + } + }) + } + + fun setDynamicDrawerSized(drawer: (canvas: Canvas, frameIndex: Int, width: Int, height: Int) -> Boolean, forKey: String) { + this.dynamicDrawerSized.put(forKey, drawer) + } + + fun clearDynamicObjects() { + this.isTextDirty = true + this.dynamicHidden.clear() + this.dynamicImage.clear() + this.dynamicText.clear() + this.dynamicTextPaint.clear() + this.dynamicStaticLayoutText.clear() + this.dynamicBoringLayoutText.clear() + this.dynamicDrawer.clear() + this.dynamicIClickArea.clear() + this.mClickMap.clear() + this.dynamicDrawerSized.clear() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt b/library/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt new file mode 100644 index 0000000..2b8efc3 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt @@ -0,0 +1,329 @@ +package com.opensource.svgaplayer + +import android.animation.Animator +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.animation.LinearInterpolator +import com.chwl.library.R +import com.opensource.svgaplayer.utils.SVGARange +import com.opensource.svgaplayer.utils.log.LogUtils +import java.lang.ref.WeakReference +import java.net.URL + +/** + * Created by PonyCui on 2017/3/29. + */ +open class SVGAImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { + + private val TAG = "SVGAImageView" + + enum class FillMode { + Backward, + Forward, + Clear, + } + + var isAnimating = false + private set + + var loops = 0 + + @Deprecated( + "It is recommended to use clearAfterDetached, or manually call to SVGAVideoEntity#clear." + + "If you just consider cleaning up the canvas after playing, you can use FillMode#Clear.", + level = DeprecationLevel.WARNING + ) + var clearsAfterStop = false + var clearsAfterDetached = false + var fillMode: FillMode = FillMode.Forward + var callback: SVGACallback? = null + + private var mAnimator: ValueAnimator? = null + private var mItemClickAreaListener: SVGAClickAreaListener? = null + private var mAntiAlias = true + private var mAutoPlay = true + private val mAnimatorListener = AnimatorListener(this) + private val mAnimatorUpdateListener = AnimatorUpdateListener(this) + private var mStartFrame = 0 + private var mEndFrame = 0 + + init { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + attrs?.let { loadAttrs(it) } + } + + private fun loadAttrs(attrs: AttributeSet) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SVGAImageView, 0, 0) + loops = typedArray.getInt(R.styleable.SVGAImageView_loopCount, 0) + clearsAfterStop = typedArray.getBoolean(R.styleable.SVGAImageView_clearsAfterStop, false) + clearsAfterDetached = typedArray.getBoolean(R.styleable.SVGAImageView_clearsAfterDetached, false) + mAntiAlias = typedArray.getBoolean(R.styleable.SVGAImageView_antiAlias, true) + mAutoPlay = typedArray.getBoolean(R.styleable.SVGAImageView_autoPlay, true) + typedArray.getString(R.styleable.SVGAImageView_fillMode)?.let { + when (it) { + "0" -> { + fillMode = FillMode.Backward + } + "1" -> { + fillMode = FillMode.Forward + } + "2" -> { + fillMode = FillMode.Clear + } + } + } + typedArray.getString(R.styleable.SVGAImageView_source)?.let { + parserSource(it) + } + typedArray.recycle() + } + + private fun parserSource(source: String) { + val refImgView = WeakReference(this) + val parser = SVGAParser(context) + if (source.startsWith("http://") || source.startsWith("https://")) { + parser.decodeFromURL(URL(source), createParseCompletion(refImgView)) + } else { + parser.decodeFromAssets(source, createParseCompletion(refImgView)) + } + } + + private fun createParseCompletion(ref: WeakReference): SVGAParser.ParseCompletion { + return object : SVGAParser.ParseCompletion { + override fun onComplete(videoItem: SVGAVideoEntity) { + ref.get()?.startAnimation(videoItem) + } + + override fun onError() {} + } + } + + private fun startAnimation(videoItem: SVGAVideoEntity) { + this@SVGAImageView.post { + videoItem.antiAlias = mAntiAlias + setVideoItem(videoItem) + getSVGADrawable()?.scaleType = scaleType + if (mAutoPlay) { + startAnimation() + } + } + } + + fun startAnimation() { + startAnimation(null, false) + } + + fun startAnimation(range: SVGARange?, reverse: Boolean = false) { + stopAnimation(false) + play(range, reverse) + } + + private fun play(range: SVGARange?, reverse: Boolean) { + LogUtils.info(TAG, "================ start animation ================") + val drawable = getSVGADrawable() ?: return + setupDrawable() + mStartFrame = Math.max(0, range?.location ?: 0) + val videoItem = drawable.videoItem + mEndFrame = Math.min(videoItem.frames - 1, ((range?.location ?: 0) + (range?.length ?: Int.MAX_VALUE) - 1)) + val animator = ValueAnimator.ofInt(mStartFrame, mEndFrame) + animator.interpolator = LinearInterpolator() + animator.duration = ((mEndFrame - mStartFrame + 1) * (1000 / videoItem.FPS) / generateScale()).toLong() + animator.repeatCount = if (loops <= 0) 99999 else loops - 1 + animator.addUpdateListener(mAnimatorUpdateListener) + animator.addListener(mAnimatorListener) + if (reverse) { + animator.reverse() + } else { + animator.start() + } + mAnimator = animator + } + + private fun setupDrawable() { + val drawable = getSVGADrawable() ?: return + drawable.cleared = false + drawable.scaleType = scaleType + } + + private fun getSVGADrawable(): SVGADrawable? { + return drawable as? SVGADrawable + } + + @Suppress("UNNECESSARY_SAFE_CALL") + private fun generateScale(): Double { + var scale = 1.0 + try { + val animatorClass = Class.forName("android.animation.ValueAnimator") ?: return scale + val getMethod = animatorClass.getDeclaredMethod("getDurationScale") ?: return scale + scale = (getMethod.invoke(animatorClass) as Float).toDouble() + if (scale == 0.0) { + val setMethod = animatorClass.getDeclaredMethod("setDurationScale",Float::class.java) ?: return scale + setMethod.isAccessible = true + setMethod.invoke(animatorClass,1.0f) + scale = 1.0 + LogUtils.info(TAG, + "The animation duration scale has been reset to" + + " 1.0x, because you closed it on developer options.") + } + } catch (ignore: Exception) { + ignore.printStackTrace() + } + return scale + } + + private fun onAnimatorUpdate(animator: ValueAnimator?) { + val drawable = getSVGADrawable() ?: return + drawable.currentFrame = animator?.animatedValue as Int + val percentage = (drawable.currentFrame + 1).toDouble() / drawable.videoItem.frames.toDouble() + callback?.onStep(drawable.currentFrame, percentage) + } + + private fun onAnimationEnd(animation: Animator?) { + isAnimating = false + stopAnimation() + val drawable = getSVGADrawable() + if (drawable != null) { + when (fillMode) { + FillMode.Backward -> { + drawable.currentFrame = mStartFrame + } + FillMode.Forward -> { + drawable.currentFrame = mEndFrame + } + FillMode.Clear -> { + drawable.cleared = true + } + } + } + callback?.onFinished() + } + + fun clear() { + getSVGADrawable()?.cleared = true + getSVGADrawable()?.clear() + // 清除对 drawable 的引用 + setImageDrawable(null) + } + + fun pauseAnimation() { + stopAnimation(false) + callback?.onPause() + } + + fun stopAnimation() { + stopAnimation(clear = clearsAfterStop) + } + + fun stopAnimation(clear: Boolean) { + mAnimator?.cancel() + mAnimator?.removeAllListeners() + mAnimator?.removeAllUpdateListeners() + getSVGADrawable()?.stop() + getSVGADrawable()?.cleared = clear + } + + fun setVideoItem(videoItem: SVGAVideoEntity?) { + setVideoItem(videoItem, SVGADynamicEntity()) + } + + fun setVideoItem(videoItem: SVGAVideoEntity?, dynamicItem: SVGADynamicEntity?) { + if (videoItem == null) { + setImageDrawable(null) + } else { + val drawable = SVGADrawable(videoItem, dynamicItem ?: SVGADynamicEntity()) + drawable.cleared = true + setImageDrawable(drawable) + } + } + + fun stepToFrame(frame: Int, andPlay: Boolean) { + pauseAnimation() + val drawable = getSVGADrawable() ?: return + drawable.currentFrame = frame + if (andPlay) { + startAnimation() + mAnimator?.let { + it.currentPlayTime = (Math.max(0.0f, Math.min(1.0f, (frame.toFloat() / drawable.videoItem.frames.toFloat()))) * it.duration).toLong() + } + } + } + + fun stepToPercentage(percentage: Double, andPlay: Boolean) { + val drawable = drawable as? SVGADrawable ?: return + var frame = (drawable.videoItem.frames * percentage).toInt() + if (frame >= drawable.videoItem.frames && frame > 0) { + frame = drawable.videoItem.frames - 1 + } + stepToFrame(frame, andPlay) + } + + fun setOnAnimKeyClickListener(clickListener : SVGAClickAreaListener){ + mItemClickAreaListener = clickListener + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action != MotionEvent.ACTION_DOWN) { + return super.onTouchEvent(event) + } + val drawable = getSVGADrawable() ?: return super.onTouchEvent(event) + for ((key, value) in drawable.dynamicItem.mClickMap) { + if (event.x >= value[0] && event.x <= value[2] && event.y >= value[1] && event.y <= value[3]) { + mItemClickAreaListener?.let { + it.onClick(key) + return true + } + } + } + + return super.onTouchEvent(event) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopAnimation(clearsAfterDetached) + if (clearsAfterDetached) { + clear() + } + } + + private class AnimatorListener(view: SVGAImageView) : Animator.AnimatorListener { + private val weakReference = WeakReference(view) + + override fun onAnimationRepeat(animation: Animator) { + weakReference.get()?.callback?.onRepeat() + } + + override fun onAnimationEnd(animation: Animator) { + weakReference.get()?.onAnimationEnd(animation) + } + + override fun onAnimationCancel(animation: Animator) { + weakReference.get()?.isAnimating = false + } + + override fun onAnimationStart(animation: Animator) { + weakReference.get()?.isAnimating = true + } + } // end of AnimatorListener + + + private class AnimatorUpdateListener(view: SVGAImageView) : ValueAnimator.AnimatorUpdateListener { + private val weakReference = WeakReference(view) + + override fun onAnimationUpdate(animation: ValueAnimator) { + weakReference.get()?.onAnimatorUpdate(animation) + } + } // end of AnimatorUpdateListener +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGAParser.kt b/library/src/main/java/com/opensource/svgaplayer/SVGAParser.kt new file mode 100644 index 0000000..2b711ef --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGAParser.kt @@ -0,0 +1,565 @@ +package com.opensource.svgaplayer + +import android.content.Context +import android.net.http.HttpResponseCache +import android.os.Handler +import android.os.Looper +import com.opensource.svgaplayer.proto.MovieEntity +import com.opensource.svgaplayer.utils.log.LogUtils +import org.json.JSONObject +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.Executors +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.Inflater +import java.util.zip.ZipInputStream + +/** + * Created by PonyCui 16/6/18. + */ +private var fileLock: Int = 0 +private var isUnzipping = false + +class SVGAParser(context: Context?) { + private var mContext = context?.applicationContext + + init { + SVGACache.onCreate(context) + } + + @Volatile + private var mFrameWidth: Int = 0 + + @Volatile + private var mFrameHeight: Int = 0 + + interface ParseCompletion { + fun onComplete(videoItem: SVGAVideoEntity) + fun onError() + } + + interface PlayCallback{ + fun onPlay(file: List) + } + + open class FileDownloader { + + var noCache = false + + open fun resume(url: URL, complete: (inputStream: InputStream) -> Unit, failure: (e: Exception) -> Unit): () -> Unit { + var cancelled = false + val cancelBlock = { + cancelled = true + } + threadPoolExecutor.execute { + try { + LogUtils.info(TAG, "================ svga file download start ================") + if (HttpResponseCache.getInstalled() == null && !noCache) { + LogUtils.error(TAG, "SVGAParser can not handle cache before install HttpResponseCache. see https://github.com/yyued/SVGAPlayer-Android#cache") + LogUtils.error(TAG, "在配置 HttpResponseCache 前 SVGAParser 无法缓存. 查看 https://github.com/yyued/SVGAPlayer-Android#cache ") + } + (url.openConnection() as? HttpURLConnection)?.let { + it.connectTimeout = 20 * 1000 + it.requestMethod = "GET" + it.setRequestProperty("Connection", "close") + it.connect() + it.inputStream.use { inputStream -> + ByteArrayOutputStream().use { outputStream -> + val buffer = ByteArray(4096) + var count: Int + while (true) { + if (cancelled) { + LogUtils.warn(TAG, "================ svga file download canceled ================") + break + } + count = inputStream.read(buffer, 0, 4096) + if (count == -1) { + break + } + outputStream.write(buffer, 0, count) + } + if (cancelled) { + LogUtils.warn(TAG, "================ svga file download canceled ================") + return@execute + } + ByteArrayInputStream(outputStream.toByteArray()).use { + LogUtils.info(TAG, "================ svga file download complete ================") + complete(it) + } + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "================ svga file download fail ================") + LogUtils.error(TAG, "error: ${e.message}") + e.printStackTrace() + failure(e) + } + } + return cancelBlock + } + } + + var fileDownloader = FileDownloader() + + companion object { + private const val TAG = "SVGAParser" + + private val threadNum = AtomicInteger(0) + private var mShareParser = SVGAParser(null) + + internal var threadPoolExecutor = Executors.newCachedThreadPool { r -> + Thread(r, "SVGAParser-Thread-${threadNum.getAndIncrement()}") + } + + fun setThreadPoolExecutor(executor: ThreadPoolExecutor) { + threadPoolExecutor = executor + } + + fun shareParser(): SVGAParser { + return mShareParser + } + } + + fun init(context: Context) { + mContext = context.applicationContext + SVGACache.onCreate(mContext) + } + + fun setFrameSize(frameWidth: Int, frameHeight: Int) { + mFrameWidth = frameWidth + mFrameHeight = frameHeight + } + + fun decodeFromAssets( + name: String, + callback: ParseCompletion?, + playCallback: PlayCallback? = null + ) { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + LogUtils.info(TAG, "================ decode $name from assets ================") + threadPoolExecutor.execute { + try { + mContext?.assets?.open(name)?.let { + this.decodeFromInputStream( + it, + SVGACache.buildCacheKey("file:///assets/$name"), + callback, + true, + playCallback, + alias = name + ) + } + } catch (e: Exception) { + this.invokeErrorCallback(e, callback, name) + } + } + + } + + fun decodeFromURL( + url: URL, + callback: ParseCompletion?, + playCallback: PlayCallback? = null + ): (() -> Unit)? { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return null + } + val urlPath = url.toString() + LogUtils.info(TAG, "================ decode from url: $urlPath ================") + val cacheKey = SVGACache.buildCacheKey(url); + return if (SVGACache.isCached(cacheKey)) { + LogUtils.info(TAG, "this url cached") + threadPoolExecutor.execute { + if (SVGACache.isDefaultCache()) { + this.decodeFromCacheKey(cacheKey, callback, alias = urlPath) + } else { + this.decodeFromSVGAFileCacheKey(cacheKey, callback, playCallback, alias = urlPath) + } + } + return null + } else { + LogUtils.info(TAG, "no cached, prepare to download") + fileDownloader.resume(url, { + this.decodeFromInputStream( + it, + cacheKey, + callback, + false, + playCallback, + alias = urlPath + ) + }, { + LogUtils.error( + TAG, + "================ svga file: $url download fail ================" + ) + this.invokeErrorCallback(it, callback, alias = urlPath) + }) + } + } + + /** + * 读取解析本地缓存的 svga 文件. + */ + fun decodeFromSVGAFileCacheKey( + cacheKey: String, + callback: ParseCompletion?, + playCallback: PlayCallback?, + alias: String? = null + ) { + threadPoolExecutor.execute { + try { + LogUtils.info(TAG, "================ decode $alias from svga cachel file to entity ================") + FileInputStream(SVGACache.buildSvgaFile(cacheKey)).use { inputStream -> + readAsBytes(inputStream)?.let { bytes -> + if (isZipFile(bytes)) { + this.decodeFromCacheKey(cacheKey, callback, alias) + } else { + LogUtils.info(TAG, "inflate start") + inflate(bytes)?.let { + LogUtils.info(TAG, "inflate complete") + val videoItem = SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + File(cacheKey), + mFrameWidth, + mFrameHeight + ) + LogUtils.info(TAG, "SVGAVideoEntity prepare start") + videoItem.prepare({ + LogUtils.info(TAG, "SVGAVideoEntity prepare success") + this.invokeCompleteCallback(videoItem, callback, alias) + },playCallback) + + } ?: this.invokeErrorCallback( + Exception("inflate(bytes) cause exception"), + callback, + alias + ) + } + } ?: this.invokeErrorCallback( + Exception("readAsBytes(inputStream) cause exception"), + callback, + alias + ) + } + } catch (e: java.lang.Exception) { + this.invokeErrorCallback(e, callback, alias) + } finally { + LogUtils.info(TAG, "================ decode $alias from svga cachel file to entity end ================") + } + } + } + + fun decodeFromInputStream( + inputStream: InputStream, + cacheKey: String, + callback: ParseCompletion?, + closeInputStream: Boolean = false, + playCallback: PlayCallback? = null, + alias: String? = null + ) { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + LogUtils.info(TAG, "================ decode $alias from input stream ================") + threadPoolExecutor.execute { + try { + readAsBytes(inputStream)?.let { bytes -> + if (isZipFile(bytes)) { + LogUtils.info(TAG, "decode from zip file") + if (!SVGACache.buildCacheDir(cacheKey).exists() || isUnzipping) { + synchronized(fileLock) { + if (!SVGACache.buildCacheDir(cacheKey).exists()) { + isUnzipping = true + LogUtils.info(TAG, "no cached, prepare to unzip") + ByteArrayInputStream(bytes).use { + unzip(it, cacheKey) + isUnzipping = false + LogUtils.info(TAG, "unzip success") + } + } + } + } + this.decodeFromCacheKey(cacheKey, callback, alias) + } else { + if (!SVGACache.isDefaultCache()) { + // 如果 SVGACache 设置类型为 FILE + threadPoolExecutor.execute { + SVGACache.buildSvgaFile(cacheKey).let { cacheFile -> + try { + cacheFile.takeIf { !it.exists() }?.createNewFile() + FileOutputStream(cacheFile).write(bytes) + } catch (e: Exception) { + LogUtils.error(TAG, "create cache file fail.", e) + cacheFile.delete() + } + } + } + } + LogUtils.info(TAG, "inflate start") + inflate(bytes)?.let { + LogUtils.info(TAG, "inflate complete") + val videoItem = SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + File(cacheKey), + mFrameWidth, + mFrameHeight + ) + LogUtils.info(TAG, "SVGAVideoEntity prepare start") + videoItem.prepare({ + LogUtils.info(TAG, "SVGAVideoEntity prepare success") + this.invokeCompleteCallback(videoItem, callback, alias) + },playCallback) + + } ?: this.invokeErrorCallback( + Exception("inflate(bytes) cause exception"), + callback, + alias + ) + } + } ?: this.invokeErrorCallback( + Exception("readAsBytes(inputStream) cause exception"), + callback, + alias + ) + } catch (e: java.lang.Exception) { + this.invokeErrorCallback(e, callback, alias) + } finally { + if (closeInputStream) { + inputStream.close() + } + LogUtils.info(TAG, "================ decode $alias from input stream end ================") + } + } + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromAssets(assetsName, callback)")) + fun parse(assetsName: String, callback: ParseCompletion?) { + this.decodeFromAssets(assetsName, callback,null) + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromURL(url, callback)")) + fun parse(url: URL, callback: ParseCompletion?) { + this.decodeFromURL(url, callback,null) + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromInputStream(inputStream, cacheKey, callback, closeInputStream)")) + fun parse( + inputStream: InputStream, + cacheKey: String, + callback: ParseCompletion?, + closeInputStream: Boolean = false + ) { + this.decodeFromInputStream(inputStream, cacheKey, callback, closeInputStream,null) + } + + private fun invokeCompleteCallback( + videoItem: SVGAVideoEntity, + callback: ParseCompletion?, + alias: String? + ) { + Handler(Looper.getMainLooper()).post { + LogUtils.info(TAG, "================ $alias parser complete ================") + callback?.onComplete(videoItem) + } + } + + private fun invokeErrorCallback( + e: Exception, + callback: ParseCompletion?, + alias: String? + ) { + e.printStackTrace() + LogUtils.error(TAG, "================ $alias parser error ================") + LogUtils.error(TAG, "$alias parse error", e) + Handler(Looper.getMainLooper()).post { + callback?.onError() + } + } + + private fun decodeFromCacheKey( + cacheKey: String, + callback: ParseCompletion?, + alias: String? + ) { + LogUtils.info(TAG, "================ decode $alias from cache ================") + LogUtils.debug(TAG, "decodeFromCacheKey called with cacheKey : $cacheKey") + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + try { + val cacheDir = SVGACache.buildCacheDir(cacheKey) + File(cacheDir, "movie.binary").takeIf { it.isFile }?.let { binaryFile -> + try { + LogUtils.info(TAG, "binary change to entity") + FileInputStream(binaryFile).use { + LogUtils.info(TAG, "binary change to entity success") + this.invokeCompleteCallback( + SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + cacheDir, + mFrameWidth, + mFrameHeight + ), + callback, + alias + ) + } + + } catch (e: Exception) { + LogUtils.error(TAG, "binary change to entity fail", e) + cacheDir.delete() + binaryFile.delete() + throw e + } + } + File(cacheDir, "movie.spec").takeIf { it.isFile }?.let { jsonFile -> + try { + LogUtils.info(TAG, "spec change to entity") + FileInputStream(jsonFile).use { fileInputStream -> + ByteArrayOutputStream().use { byteArrayOutputStream -> + val buffer = ByteArray(2048) + while (true) { + val size = fileInputStream.read(buffer, 0, buffer.size) + if (size == -1) { + break + } + byteArrayOutputStream.write(buffer, 0, size) + } + byteArrayOutputStream.toString().let { + JSONObject(it).let { + LogUtils.info(TAG, "spec change to entity success") + this.invokeCompleteCallback( + SVGAVideoEntity( + it, + cacheDir, + mFrameWidth, + mFrameHeight + ), + callback, + alias + ) + } + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "$alias movie.spec change to entity fail", e) + cacheDir.delete() + jsonFile.delete() + throw e + } + } + } catch (e: Exception) { + this.invokeErrorCallback(e, callback, alias) + } + } + + private fun readAsBytes(inputStream: InputStream): ByteArray? { + ByteArrayOutputStream().use { byteArrayOutputStream -> + val byteArray = ByteArray(2048) + while (true) { + val count = inputStream.read(byteArray, 0, 2048) + if (count <= 0) { + break + } else { + byteArrayOutputStream.write(byteArray, 0, count) + } + } + return byteArrayOutputStream.toByteArray() + } + } + + private fun inflate(byteArray: ByteArray): ByteArray? { + val inflater = Inflater() + inflater.setInput(byteArray, 0, byteArray.size) + val inflatedBytes = ByteArray(2048) + ByteArrayOutputStream().use { inflatedOutputStream -> + while (true) { + val count = inflater.inflate(inflatedBytes, 0, 2048) + if (count <= 0) { + break + } else { + inflatedOutputStream.write(inflatedBytes, 0, count) + } + } + inflater.end() + return inflatedOutputStream.toByteArray() + } + } + + // 是否是 zip 文件 + private fun isZipFile(bytes: ByteArray): Boolean { + return bytes.size > 4 && bytes[0].toInt() == 80 && bytes[1].toInt() == 75 && bytes[2].toInt() == 3 && bytes[3].toInt() == 4 + } + + // 解压 + private fun unzip(inputStream: InputStream, cacheKey: String) { + LogUtils.info(TAG, "================ unzip prepare ================") + val cacheDir = SVGACache.buildCacheDir(cacheKey) + cacheDir.mkdirs() + try { + BufferedInputStream(inputStream).use { + ZipInputStream(it).use { zipInputStream -> + while (true) { + val zipItem = zipInputStream.nextEntry ?: break + if (zipItem.name.contains("../")) { + // 解压路径存在路径穿越问题,直接过滤 + continue + } + if (zipItem.name.contains("/")) { + continue + } + val file = File(cacheDir, zipItem.name) + ensureUnzipSafety(file, cacheDir.absolutePath) + FileOutputStream(file).use { fileOutputStream -> + val buff = ByteArray(2048) + while (true) { + val readBytes = zipInputStream.read(buff) + if (readBytes <= 0) { + break + } + fileOutputStream.write(buff, 0, readBytes) + } + } + LogUtils.error(TAG, "================ unzip complete ================") + zipInputStream.closeEntry() + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "================ unzip error ================") + LogUtils.error(TAG, "error", e) + SVGACache.clearDir(cacheDir.absolutePath) + cacheDir.delete() + throw e + } + } + + // 检查 zip 路径穿透 + private fun ensureUnzipSafety(outputFile: File, dstDirPath: String) { + val dstDirCanonicalPath = File(dstDirPath).canonicalPath + val outputFileCanonicalPath = outputFile.canonicalPath + if (!outputFileCanonicalPath.startsWith(dstDirCanonicalPath)) { + throw IOException("Found Zip Path Traversal Vulnerability with $dstDirCanonicalPath") + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt b/library/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt new file mode 100644 index 0000000..57ade03 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt @@ -0,0 +1,19 @@ +package com.opensource.svgaplayer + +import android.content.Context +import android.util.AttributeSet + +/** + * Created by cuiminghui on 2017/3/30. + * @deprecated from 2.4.0 + */ +@Deprecated("This class has been deprecated from 2.4.0. We don't recommend you to use it.") +class SVGAPlayer: SVGAImageView { + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {} + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} + +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt b/library/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt new file mode 100644 index 0000000..18719df --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt @@ -0,0 +1,194 @@ +package com.opensource.svgaplayer + +/** + * @author Devin + * + * Created on 2/24/21. + */ +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import com.opensource.svgaplayer.utils.log.LogUtils +import java.io.FileDescriptor + +/** + * Author : llk + * Time : 2020/10/24 + * Description : svga 音频加载管理类 + * 将 SoundPool 抽取到单例里边,规避 load 资源之后不回调 onLoadComplete 的问题。 + * + * 需要对 SVGASoundManager 进行初始化 + * + * 相关文章:Android SoundPool 崩溃问题研究 + * https://zhuanlan.zhihu.com/p/29985198 + */ +object SVGASoundManager { + + private val TAG = SVGASoundManager::class.java.simpleName + + private var soundPool: SoundPool? = null + + private val soundCallBackMap: MutableMap = mutableMapOf() + + /** + * 音量设置,范围在 [0, 1] 之间 + */ + private var volume: Float = 1f + + /** + * 音频回调 + */ + internal interface SVGASoundCallBack { + + // 音量发生变化 + fun onVolumeChange(value: Float) + + // 音频加载完成 + fun onComplete() + } + + fun init() { + init(20) + } + + fun init(maxStreams: Int) { + LogUtils.debug(TAG, "**************** init **************** $maxStreams") + if (soundPool != null) { + return + } + soundPool = getSoundPool(maxStreams) + soundPool?.setOnLoadCompleteListener { _, soundId, status -> + LogUtils.debug(TAG, "SoundPool onLoadComplete soundId=$soundId status=$status") + if (status == 0) { //加载该声音成功 + if (soundCallBackMap.containsKey(soundId)) { + soundCallBackMap[soundId]?.onComplete() + } + } + } + } + + fun release() { + LogUtils.debug(TAG, "**************** release ****************") + if (soundCallBackMap.isNotEmpty()) { + soundCallBackMap.clear() + } + } + + /** + * 根据当前播放实体,设置音量 + * + * @param volume 范围在 [0, 1] + * @param entity 根据需要控制对应 entity 音量大小,若为空则控制所有正在播放的音频音量 + */ + fun setVolume(volume: Float, entity: SVGAVideoEntity? = null) { + if (!checkInit()) { + return + } + + if (volume < 0f || volume > 1f) { + LogUtils.error(TAG, "The volume level is in the range of 0 to 1 ") + return + } + + if (entity == null) { + this.volume = volume + val iterator = soundCallBackMap.entries.iterator() + while (iterator.hasNext()) { + val e = iterator.next() + e.value.onVolumeChange(volume) + } + return + } + + val soundPool = soundPool ?: return + + entity.audioList.forEach { audio -> + val streamId = audio.playID ?: return + soundPool.setVolume(streamId, volume, volume) + } + } + + /** + * 是否初始化 + * 如果没有初始化,就使用原来SvgaPlayer库的音频加载逻辑。 + * @return true 则已初始化, 否则为 false + */ + internal fun isInit(): Boolean { + return soundPool != null + } + + private fun checkInit(): Boolean { + val isInit = isInit() + if (!isInit) { + LogUtils.error(TAG, "soundPool is null, you need call init() !!!") + } + return isInit + } + + private fun getSoundPool(maxStreams: Int) = if (Build.VERSION.SDK_INT >= 21) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + SoundPool.Builder().setAudioAttributes(attributes) + .setMaxStreams(maxStreams) + .build() + } else { + SoundPool(maxStreams, AudioManager.STREAM_MUSIC, 0) + } + + internal fun load(callBack: SVGASoundCallBack?, + fd: FileDescriptor?, + offset: Long, + length: Long, + priority: Int): Int { + if (!checkInit()) return -1 + + val soundId = soundPool!!.load(fd, offset, length, priority) + + LogUtils.debug(TAG, "load soundId=$soundId callBack=$callBack") + + if (callBack != null && !soundCallBackMap.containsKey(soundId)) { + soundCallBackMap[soundId] = callBack + } + return soundId + } + + internal fun unload(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "unload soundId=$soundId") + + soundPool!!.unload(soundId) + + soundCallBackMap.remove(soundId) + } + + internal fun play(soundId: Int): Int { + if (!checkInit()) return -1 + + LogUtils.debug(TAG, "play soundId=$soundId") + return soundPool!!.play(soundId, volume, volume, 1, 0, 1.0f) + } + + internal fun stop(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "stop soundId=$soundId") + soundPool!!.stop(soundId) + } + + internal fun resume(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "stop soundId=$soundId") + soundPool!!.resume(soundId) + } + + internal fun pause(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "pause soundId=$soundId") + soundPool!!.pause(soundId) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt b/library/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt new file mode 100644 index 0000000..6a9c281 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt @@ -0,0 +1,345 @@ +package com.opensource.svgaplayer + +import android.graphics.Bitmap +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import com.chwl.library.common.util.isVerify +import com.opensource.svgaplayer.bitmap.SVGABitmapByteArrayDecoder +import com.opensource.svgaplayer.bitmap.SVGABitmapFileDecoder +import com.opensource.svgaplayer.entities.SVGAAudioEntity +import com.opensource.svgaplayer.entities.SVGAVideoSpriteEntity +import com.opensource.svgaplayer.proto.AudioEntity +import com.opensource.svgaplayer.proto.MovieEntity +import com.opensource.svgaplayer.proto.MovieParams +import com.opensource.svgaplayer.utils.SVGARect +import com.opensource.svgaplayer.utils.log.LogUtils +import org.json.JSONObject +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +/** + * Created by PonyCui on 16/6/18. + */ +class SVGAVideoEntity { + + private val TAG = "SVGAVideoEntity" + + var antiAlias = true + var movieItem: MovieEntity? = null + + var videoSize = SVGARect(0.0, 0.0, 0.0, 0.0) + private set + + var FPS = 15 + private set + + var frames: Int = 0 + private set + + internal var spriteList: List = emptyList() + internal var audioList: List = emptyList() + internal var soundPool: SoundPool? = null + private var soundCallback: SVGASoundManager.SVGASoundCallBack? = null + internal var imageMap = HashMap() + private var mCacheDir: File + private var mFrameHeight = 0 + private var mFrameWidth = 0 + private var mPlayCallback: SVGAParser.PlayCallback?=null + private lateinit var mCallback: () -> Unit + + constructor(json: JSONObject, cacheDir: File) : this(json, cacheDir, 0, 0) + constructor(entity: MovieEntity, cacheDir: File) : this(entity, cacheDir, 0, 0) + + constructor(json: JSONObject, cacheDir: File, frameWidth: Int, frameHeight: Int) { + mFrameWidth = frameWidth + mFrameHeight = frameHeight + mCacheDir = cacheDir + val movieJsonObject = json.optJSONObject("movie") ?: return + setupByJson(movieJsonObject) + try { + parserImages(json) + } catch (e: Exception) { + e.printStackTrace() + } catch (e: OutOfMemoryError) { + e.printStackTrace() + } + resetSprites(json) + } + constructor(entity: MovieEntity, cacheDir: File, frameWidth: Int, frameHeight: Int) { + this.mFrameWidth = frameWidth + this.mFrameHeight = frameHeight + this.mCacheDir = cacheDir + this.movieItem = entity + entity.params?.let(this::setupByMovie) + try { + parserImages(entity) + } catch (e: Exception) { + e.printStackTrace() + } catch (e: OutOfMemoryError) { + e.printStackTrace() + } + resetSprites(entity) + } + + private fun setupByJson(movieObject: JSONObject) { + movieObject.optJSONObject("viewBox")?.let { viewBoxObject -> + val width = viewBoxObject.optDouble("width", 0.0) + val height = viewBoxObject.optDouble("height", 0.0) + videoSize = SVGARect(0.0, 0.0, width, height) + } + FPS = movieObject.optInt("fps", 20) + frames = movieObject.optInt("frames", 0) + } + + private fun setupByMovie(movieParams: MovieParams) { + val width = (movieParams.viewBoxWidth ?: 0.0f).toDouble() + val height = (movieParams.viewBoxHeight ?: 0.0f).toDouble() + videoSize = SVGARect(0.0, 0.0, width, height) + FPS = movieParams.fps ?: 20 + frames = movieParams.frames ?: 0 + } + + internal fun prepare(callback: () -> Unit, playCallback: SVGAParser.PlayCallback?) { + mCallback = callback + mPlayCallback = playCallback + if (movieItem == null) { + mCallback() + } else { + setupAudios(movieItem!!) { + mCallback() + } + } + } + + private fun parserImages(json: JSONObject) { + val imgJson = json.optJSONObject("images") ?: return + imgJson.keys().forEach { imgKey -> + val filePath = generateBitmapFilePath(imgJson[imgKey].toString(), imgKey) + if (filePath.isEmpty()) { + return + } + val bitmapKey = imgKey.replace(".matte", "") + val bitmap = createBitmap(filePath) + if (bitmap != null) { + imageMap[bitmapKey] = bitmap + } + } + } + + private fun generateBitmapFilePath(imgName: String, imgKey: String): String { + val path = mCacheDir.absolutePath + "/" + imgName + val path1 = "$path.png" + val path2 = mCacheDir.absolutePath + "/" + imgKey + ".png" + + return when { + File(path).exists() -> path + File(path1).exists() -> path1 + File(path2).exists() -> path2 + else -> "" + } + } + + private fun createBitmap(filePath: String): Bitmap? { + return SVGABitmapFileDecoder.decodeBitmapFrom(filePath, mFrameWidth, mFrameHeight) + } + + private fun parserImages(obj: MovieEntity) { + obj.images?.entries?.forEach { entry -> + val byteArray = entry.value.toByteArray() + if (byteArray.count() < 4) { + return@forEach + } + val fileTag = byteArray.slice(IntRange(0, 3)) + if (fileTag[0].toInt() == 73 && fileTag[1].toInt() == 68 && fileTag[2].toInt() == 51) { + return@forEach + } + val filePath = generateBitmapFilePath(entry.value.utf8(), entry.key) + createBitmap(byteArray, filePath)?.let { bitmap -> + imageMap[entry.key] = bitmap + } + } + } + + private fun createBitmap(byteArray: ByteArray, filePath: String): Bitmap? { + val bitmap = SVGABitmapByteArrayDecoder.decodeBitmapFrom(byteArray, mFrameWidth, mFrameHeight) + return bitmap ?: createBitmap(filePath) + } + + private fun resetSprites(json: JSONObject) { + val mutableList: MutableList = mutableListOf() + json.optJSONArray("sprites")?.let { item -> + for (i in 0 until item.length()) { + item.optJSONObject(i)?.let { entryJson -> + mutableList.add(SVGAVideoSpriteEntity(entryJson)) + } + } + } + spriteList = mutableList.toList() + } + + private fun resetSprites(entity: MovieEntity) { + spriteList = entity.sprites?.map { + return@map SVGAVideoSpriteEntity(it) + } ?: listOf() + } + + private fun setupAudios(entity: MovieEntity, completionBlock: () -> Unit) { + if (entity.audios == null || entity.audios.isEmpty()) { + run(completionBlock) + return + } + setupSoundPool(entity, completionBlock) + val audiosFileMap = generateAudioFileMap(entity) + //repair when audioEntity error can not callback + //如果audiosFileMap为空 soundPool?.load 不会走 导致 setOnLoadCompleteListener 不会回调 导致外层prepare不回调卡住 + if (audiosFileMap.size == 0) { + run(completionBlock) + return + } + this.audioList = entity.audios.map { audio -> + return@map createSvgaAudioEntity(audio, audiosFileMap) + } + } + + private fun createSvgaAudioEntity(audio: AudioEntity, audiosFileMap: HashMap): SVGAAudioEntity { + val item = SVGAAudioEntity(audio) + val startTime = (audio.startTime ?: 0).toDouble() + val totalTime = (audio.totalTime ?: 0).toDouble() + if (totalTime.toInt() == 0) { + // 除数不能为 0 + return item + } + // 直接回调文件,后续播放都不走 + mPlayCallback?.let { + val fileList: MutableList = ArrayList() + audiosFileMap.forEach { entity -> + fileList.add(entity.value) + } + it.onPlay(fileList) + mCallback() + return item + } + + audiosFileMap[audio.audioKey]?.let { file -> + FileInputStream(file).use { + val length = it.available().toDouble() + val offset = ((startTime / totalTime) * length).toLong() + if (SVGASoundManager.isInit()) { + item.soundID = SVGASoundManager.load(soundCallback, + it.fd, + offset, + length.toLong(), + 1) + } else { + item.soundID = soundPool?.load(it.fd, offset, length.toLong(), 1) + } + } + } + return item + } + + private fun generateAudioFile(audioCache: File, value: ByteArray): File { + audioCache.createNewFile() + FileOutputStream(audioCache).write(value) + return audioCache + } + + private fun generateAudioFileMap(entity: MovieEntity): HashMap { + val audiosDataMap = generateAudioMap(entity) + val audiosFileMap = HashMap() + if (audiosDataMap.isNotEmpty()) { + audiosDataMap.forEach { + val audioKey = if (mCacheDir.name.isVerify()) mCacheDir.name+it.key else it.key + val audioCache = SVGACache.buildAudioFile(audioKey) + audiosFileMap[it.key] = + audioCache.takeIf { file -> file.exists() } ?: generateAudioFile( + audioCache, + it.value + ) + } + } + return audiosFileMap + } + + private fun generateAudioMap(entity: MovieEntity): HashMap { + val audiosDataMap = HashMap() + entity.images?.entries?.forEach { + val imageKey = it.key + val byteArray = it.value.toByteArray() + if (byteArray.count() < 4) { + return@forEach + } + val fileTag = byteArray.slice(IntRange(0, 3)) + if (fileTag[0].toInt() == 73 && fileTag[1].toInt() == 68 && fileTag[2].toInt() == 51) { + audiosDataMap[imageKey] = byteArray + }else if(fileTag[0].toInt() == -1 && fileTag[1].toInt() == -5 && fileTag[2].toInt() == -108){ + audiosDataMap[imageKey] = byteArray + } + } + return audiosDataMap + } + + private fun setupSoundPool(entity: MovieEntity, completionBlock: () -> Unit) { + var soundLoaded = 0 + if (SVGASoundManager.isInit()) { + soundCallback = object : SVGASoundManager.SVGASoundCallBack { + override fun onVolumeChange(value: Float) { + SVGASoundManager.setVolume(value, this@SVGAVideoEntity) + } + + override fun onComplete() { + soundLoaded++ + if (soundLoaded >= entity.audios.count()) { + completionBlock() + } + } + } + return + } + soundPool = generateSoundPool(entity) + LogUtils.info("SVGAParser", "pool_start") + soundPool?.setOnLoadCompleteListener { _, _, _ -> + LogUtils.info("SVGAParser", "pool_complete") + soundLoaded++ + if (soundLoaded >= entity.audios.count()) { + completionBlock() + } + } + } + + private fun generateSoundPool(entity: MovieEntity): SoundPool? { + return try { + if (Build.VERSION.SDK_INT >= 21) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + SoundPool.Builder().setAudioAttributes(attributes) + .setMaxStreams(12.coerceAtMost(entity.audios.count())) + .build() + } else { + SoundPool(12.coerceAtMost(entity.audios.count()), AudioManager.STREAM_MUSIC, 0) + } + } catch (e: Exception) { + LogUtils.error(TAG, e) + null + } + } + + fun clear() { + if (SVGASoundManager.isInit()) { + this.audioList.forEach { + it.soundID?.let { id -> SVGASoundManager.unload(id) } + } + soundCallback = null + } + soundPool?.release() + soundPool = null + audioList = emptyList() + spriteList = emptyList() + imageMap.clear() + } +} + diff --git a/library/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt b/library/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt new file mode 100644 index 0000000..e1ea015 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt @@ -0,0 +1,33 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.BitmapFactory + +/** + * + * Create by im_dsd 2020/7/7 17:59 + */ +internal object BitmapSampleSizeCalculator { + + fun calculate(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + // Raw height and width of image + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + + if (reqHeight <= 0 || reqWidth <= 0) { + return inSampleSize + } + if (height > reqHeight || width > reqWidth) { + + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt new file mode 100644 index 0000000..0c1dba6 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt @@ -0,0 +1,16 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * 通过字节码解码 Bitmap + * + * Create by im_dsd 2020/7/7 17:50 + */ +internal object SVGABitmapByteArrayDecoder : SVGABitmapDecoder() { + + override fun onDecode(data: ByteArray, ops: BitmapFactory.Options): Bitmap? { + return BitmapFactory.decodeByteArray(data, 0, data.count(), ops) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt new file mode 100644 index 0000000..8816fbb --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt @@ -0,0 +1,35 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * Bitmap 解码器 + * + * 需要加载的数据类型 + * + * Create by im_dsd 2020/7/7 17:39 + */ +internal abstract class SVGABitmapDecoder { + + fun decodeBitmapFrom(data: T, reqWidth: Int, reqHeight: Int): Bitmap? { + return BitmapFactory.Options().run { + // 如果期望的宽高是合法的, 则开启检测尺寸模式 + inJustDecodeBounds = (reqWidth > 0 && reqHeight > 0) + inPreferredConfig = Bitmap.Config.RGB_565 + + val bitmap = onDecode(data, this) + if (!inJustDecodeBounds) { + return bitmap + } + + // Calculate inSampleSize + inSampleSize = BitmapSampleSizeCalculator.calculate(this, reqWidth, reqHeight) + // Decode bitmap with inSampleSize set + inJustDecodeBounds = false + onDecode(data, this) + } + } + + abstract fun onDecode(data: T, ops: BitmapFactory.Options): Bitmap? +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt new file mode 100644 index 0000000..edca5bc --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt @@ -0,0 +1,16 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * 通过文件解码 Bitmap + * + * Create by im_dsd 2020/7/7 17:50 + */ +internal object SVGABitmapFileDecoder : SVGABitmapDecoder() { + + override fun onDecode(data: String, ops: BitmapFactory.Options): Bitmap? { + return BitmapFactory.decodeFile(data, ops) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt b/library/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt new file mode 100644 index 0000000..93ad384 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt @@ -0,0 +1,53 @@ +package com.opensource.svgaplayer.drawer + +import android.graphics.Canvas +import android.widget.ImageView +import com.opensource.svgaplayer.SVGAVideoEntity +import com.opensource.svgaplayer.entities.SVGAVideoSpriteFrameEntity +import com.opensource.svgaplayer.utils.Pools +import com.opensource.svgaplayer.utils.SVGAScaleInfo +import kotlin.math.max + +/** + * Created by cuiminghui on 2017/3/29. + */ + +open internal class SGVADrawer(val videoItem: SVGAVideoEntity) { + + val scaleInfo = SVGAScaleInfo() + + private val spritePool = Pools.SimplePool(max(1, videoItem.spriteList.size)) + + inner class SVGADrawerSprite(var _matteKey: String? = null, var _imageKey: String? = null, var _frameEntity: SVGAVideoSpriteFrameEntity? = null) { + val matteKey get() = _matteKey + val imageKey get() = _imageKey + val frameEntity get() = _frameEntity!! + } + + internal fun requestFrameSprites(frameIndex: Int): List { + return videoItem.spriteList.mapNotNull { + if (frameIndex >= 0 && frameIndex < it.frames.size) { + it.imageKey?.let { imageKey -> + if (!imageKey.endsWith(".matte") && it.frames[frameIndex].alpha <= 0.0) { + return@mapNotNull null + } + return@mapNotNull (spritePool.acquire() ?: SVGADrawerSprite()).apply { + _matteKey = it.matteKey + _imageKey = it.imageKey + _frameEntity = it.frames[frameIndex] + } + } + } + return@mapNotNull null + } + } + + internal fun releaseFrameSprites(sprites: List) { + sprites.forEach { spritePool.release(it) } + } + + open fun drawFrame(canvas : Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) { + scaleInfo.performScaleType(canvas.width.toFloat(),canvas.height.toFloat(), videoItem.videoSize.width.toFloat(), videoItem.videoSize.height.toFloat(), scaleType) + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt b/library/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt new file mode 100644 index 0000000..19c9ca7 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt @@ -0,0 +1,569 @@ +package com.opensource.svgaplayer.drawer + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.DashPathEffect +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.Shader +import android.os.Build +import android.text.StaticLayout +import android.text.TextUtils +import android.widget.ImageView +import com.opensource.svgaplayer.SVGADynamicEntity +import com.opensource.svgaplayer.SVGASoundManager +import com.opensource.svgaplayer.SVGAVideoEntity +import com.opensource.svgaplayer.entities.SVGAVideoShapeEntity + +/** + * Created by cuiminghui on 2017/3/29. + */ + +internal class SVGACanvasDrawer(videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity) : SGVADrawer(videoItem) { + + private val sharedValues = ShareValues() + private val drawTextCache: HashMap = hashMapOf() + private val pathCache = PathCache() + + private var beginIndexList: Array? = null + private var endIndexList: Array? = null + + override fun drawFrame(canvas: Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) { + super.drawFrame(canvas, frameIndex, scaleType) + playAudio(frameIndex) + this.pathCache.onSizeChanged(canvas) + val sprites = requestFrameSprites(frameIndex) + // Filter null sprites + if (sprites.count() <= 0) return + val matteSprites = mutableMapOf() + var saveID = -1 + beginIndexList = null + endIndexList = null + + // Filter no matte layer + var hasMatteLayer = false + sprites.get(0).imageKey?.let { + if (it.endsWith(".matte")) { + hasMatteLayer = true + } + } + sprites.forEachIndexed { index, svgaDrawerSprite -> + + // Save matte sprite + svgaDrawerSprite.imageKey?.let { + /// No matte layer included or VERSION Unsopport matte + if (!hasMatteLayer || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // Normal sprite + drawSprite(svgaDrawerSprite, canvas, frameIndex) + // Continue + return@forEachIndexed + } + /// Cache matte sprite + if (it.endsWith(".matte")) { + matteSprites.put(it, svgaDrawerSprite) + // Continue + return@forEachIndexed + } + } + /// Is matte begin + if (isMatteBegin(index, sprites)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + saveID = canvas.saveLayer(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), null) + } else { + canvas.save() + } + } + /// Normal matte + drawSprite(svgaDrawerSprite, canvas, frameIndex) + + /// Is matte end + if (isMatteEnd(index, sprites)) { + matteSprites.get(svgaDrawerSprite.matteKey)?.let { + drawSprite(it, this.sharedValues.shareMatteCanvas(canvas.width, canvas.height), frameIndex) + canvas.drawBitmap(this.sharedValues.sharedMatteBitmap(), 0f, 0f, this.sharedValues.shareMattePaint()) + if (saveID != -1) { + canvas.restoreToCount(saveID) + } else { + canvas.restore() + } + // Continue + return@forEachIndexed + } + } + } + releaseFrameSprites(sprites) + } + + private fun isMatteBegin(spriteIndex: Int, sprites: List): Boolean { + if (beginIndexList == null) { + val boolArray = Array(sprites.count()) { false } + sprites.forEachIndexed { index, svgaDrawerSprite -> + svgaDrawerSprite.imageKey?.let { + /// Filter matte sprite + if (it.endsWith(".matte")) { + // Continue + return@forEachIndexed + } + } + svgaDrawerSprite.matteKey?.let { + if (it.length > 0) { + sprites.get(index - 1)?.let { lastSprite -> + if (lastSprite.matteKey.isNullOrEmpty()) { + boolArray[index] = true + } else { + if (lastSprite.matteKey != svgaDrawerSprite.matteKey) { + boolArray[index] = true + } + } + } + } + } + } + beginIndexList = boolArray + } + return beginIndexList?.get(spriteIndex) ?: false + } + + private fun isMatteEnd(spriteIndex: Int, sprites: List): Boolean { + if (endIndexList == null) { + val boolArray = Array(sprites.count()) { false } + sprites.forEachIndexed { index, svgaDrawerSprite -> + svgaDrawerSprite.imageKey?.let { + /// Filter matte sprite + if (it.endsWith(".matte")) { + // Continue + return@forEachIndexed + } + } + svgaDrawerSprite.matteKey?.let { + if (it.length > 0) { + // Last one + if (index == sprites.count() - 1) { + boolArray[index] = true + } else { + sprites.get(index + 1)?.let { nextSprite -> + if (nextSprite.matteKey.isNullOrEmpty()) { + boolArray[index] = true + } else { + if (nextSprite.matteKey != svgaDrawerSprite.matteKey) { + boolArray[index] = true + } + } + } + } + } + } + } + endIndexList = boolArray + } + return endIndexList?.get(spriteIndex) ?: false + } + + private fun playAudio(frameIndex: Int) { + this.videoItem.audioList.forEach { audio -> + if (audio.startFrame == frameIndex) { + if (SVGASoundManager.isInit()) { + audio.soundID?.let { soundID -> + audio.playID = SVGASoundManager.play(soundID) + } + } else { + this.videoItem.soundPool?.let { soundPool -> + audio.soundID?.let { soundID -> + audio.playID = soundPool.play(soundID, 1.0f, 1.0f, 1, 0, 1.0f) + } + } + } + + } + if (audio.endFrame <= frameIndex) { + audio.playID?.let { + if (SVGASoundManager.isInit()) { + SVGASoundManager.stop(it) + } else { + this.videoItem.soundPool?.stop(it) + } + } + audio.playID = null + } + } + } + + private fun shareFrameMatrix(transform: Matrix): Matrix { + val matrix = this.sharedValues.sharedMatrix() + matrix.postScale(scaleInfo.scaleFx, scaleInfo.scaleFy) + matrix.postTranslate(scaleInfo.tranFx, scaleInfo.tranFy) + matrix.preConcat(transform) + return matrix + } + + private fun drawSprite(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { + drawImage(sprite, canvas) + drawShape(sprite, canvas) + drawDynamic(sprite, canvas, frameIndex) + } + + private fun drawImage(sprite: SVGADrawerSprite, canvas: Canvas) { + val imageKey = sprite.imageKey ?: return + val isHidden = dynamicItem.dynamicHidden[imageKey] == true + if (isHidden) { + return + } + val bitmapKey = if (imageKey.endsWith(".matte")) imageKey.substring(0, imageKey.length - 6) else imageKey + val drawingBitmap = (dynamicItem.dynamicImage[bitmapKey] ?: videoItem.imageMap[bitmapKey]) + ?: return + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + val paint = this.sharedValues.sharedPaint() + paint.isAntiAlias = videoItem.antiAlias + paint.isFilterBitmap = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + if (sprite.frameEntity.maskPath != null) { + val maskPath = sprite.frameEntity.maskPath ?: return + canvas.save() + val path = this.sharedValues.sharedPath() + maskPath.buildPath(path) + path.transform(frameMatrix) + canvas.clipPath(path) + frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) + if (!drawingBitmap.isRecycled) { + canvas.drawBitmap(drawingBitmap, frameMatrix, paint) + } + canvas.restore() + } else { + frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) + if (!drawingBitmap.isRecycled) { + canvas.drawBitmap(drawingBitmap, frameMatrix, paint) + } + } + dynamicItem.dynamicIClickArea.let { + it.get(imageKey)?.let { listener -> + val matrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) + frameMatrix.getValues(matrixArray) + listener.onResponseArea(imageKey, matrixArray[2].toInt() + , matrixArray[5].toInt() + , (drawingBitmap.width * matrixArray[0] + matrixArray[2]).toInt() + , (drawingBitmap.height * matrixArray[4] + matrixArray[5]).toInt()) + } + } + drawTextOnBitmap(canvas, drawingBitmap, sprite, frameMatrix) + } + + private fun drawTextOnBitmap(canvas: Canvas, drawingBitmap: Bitmap, sprite: SVGADrawerSprite, frameMatrix: Matrix) { + if (dynamicItem.isTextDirty) { + this.drawTextCache.clear() + dynamicItem.isTextDirty = false + } + val imageKey = sprite.imageKey ?: return + var textBitmap: Bitmap? = null + dynamicItem.dynamicText[imageKey]?.let { drawingText -> + dynamicItem.dynamicTextPaint[imageKey]?.let { drawingTextPaint -> + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val drawRect = Rect(0, 0, drawingBitmap.width, drawingBitmap.height) + val textCanvas = Canvas(textBitmap!!) + drawingTextPaint.isAntiAlias = true + val fontMetrics = drawingTextPaint.getFontMetrics(); + val top = fontMetrics.top + val bottom = fontMetrics.bottom + val baseLineY = drawRect.centerY() - top / 2 - bottom / 2 + textCanvas.drawText(drawingText, drawRect.centerX().toFloat(), baseLineY, drawingTextPaint); + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + } + + dynamicItem.dynamicBoringLayoutText[imageKey]?.let { + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + it.paint.isAntiAlias = true + + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val textCanvas = Canvas(textBitmap!!) + textCanvas.translate(0f, ((drawingBitmap.height - it.height) / 2).toFloat()) + it.draw(textCanvas) + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + + dynamicItem.dynamicStaticLayoutText[imageKey]?.let { + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + it.paint.isAntiAlias = true + var layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + var lineMax = try { + val field = StaticLayout::class.java.getDeclaredField("mMaximumVisibleLineCount") + field.isAccessible = true + field.getInt(it) + } catch (e: Exception) { + Int.MAX_VALUE + } + StaticLayout.Builder + .obtain(it.text, 0, it.text.length, it.paint, drawingBitmap.width) + .setAlignment(it.alignment) + .setMaxLines(lineMax) + .setEllipsize(TextUtils.TruncateAt.END) + .build() + } else { + StaticLayout(it.text, 0, it.text.length, it.paint, drawingBitmap.width, it.alignment, it.spacingMultiplier, it.spacingAdd, false) + } + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val textCanvas = Canvas(textBitmap!!) + textCanvas.translate(0f, ((drawingBitmap.height - layout.height) / 2).toFloat()) + layout.draw(textCanvas) + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + textBitmap?.let { textBitmap -> + val paint = this.sharedValues.sharedPaint() + paint.isAntiAlias = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + if (sprite.frameEntity.maskPath != null) { + val maskPath = sprite.frameEntity.maskPath ?: return@let + canvas.save() + canvas.concat(frameMatrix) + canvas.clipRect(0, 0, drawingBitmap.width, drawingBitmap.height) + val bitmapShader = BitmapShader(textBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + paint.shader = bitmapShader + val path = this.sharedValues.sharedPath() + maskPath.buildPath(path) + canvas.drawPath(path, paint) + canvas.restore() + } else { + paint.isFilterBitmap = videoItem.antiAlias + canvas.drawBitmap(textBitmap, frameMatrix, paint) + } + } + } + + private fun drawShape(sprite: SVGADrawerSprite, canvas: Canvas) { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + sprite.frameEntity.shapes.forEach { shape -> + shape.buildPath() + shape.shapePath?.let { + val paint = this.sharedValues.sharedPaint() + paint.reset() + paint.isAntiAlias = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + val path = this.sharedValues.sharedPath() + path.reset() + path.addPath(this.pathCache.buildPath(shape)) + val shapeMatrix = this.sharedValues.sharedMatrix2() + shapeMatrix.reset() + shape.transform?.let { + shapeMatrix.postConcat(it) + } + shapeMatrix.postConcat(frameMatrix) + path.transform(shapeMatrix) + shape.styles?.fill?.let { + if (it != 0x00000000) { + paint.style = Paint.Style.FILL + paint.color = it + val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) + if (alpha != 255) { + paint.alpha = alpha + } + if (sprite.frameEntity.maskPath !== null) canvas.save() + sprite.frameEntity.maskPath?.let { maskPath -> + val path2 = this.sharedValues.sharedPath2() + maskPath.buildPath(path2) + path2.transform(frameMatrix) + canvas.clipPath(path2) + } + canvas.drawPath(path, paint) + if (sprite.frameEntity.maskPath !== null) canvas.restore() + } + } + shape.styles?.strokeWidth?.let { + if (it > 0) { + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + paint.style = Paint.Style.STROKE + shape.styles?.stroke?.let { + paint.color = it + val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) + if (alpha != 255) { + paint.alpha = alpha + } + } + val scale = matrixScale(frameMatrix) + shape.styles?.strokeWidth?.let { + paint.strokeWidth = it * scale + } + shape.styles?.lineCap?.let { + when { + it.equals("butt", true) -> paint.strokeCap = Paint.Cap.BUTT + it.equals("round", true) -> paint.strokeCap = Paint.Cap.ROUND + it.equals("square", true) -> paint.strokeCap = Paint.Cap.SQUARE + } + } + shape.styles?.lineJoin?.let { + when { + it.equals("miter", true) -> paint.strokeJoin = Paint.Join.MITER + it.equals("round", true) -> paint.strokeJoin = Paint.Join.ROUND + it.equals("bevel", true) -> paint.strokeJoin = Paint.Join.BEVEL + } + } + shape.styles?.miterLimit?.let { + paint.strokeMiter = it.toFloat() * scale + } + shape.styles?.lineDash?.let { + if (it.size == 3 && (it[0] > 0 || it[1] > 0)) { + paint.pathEffect = DashPathEffect(floatArrayOf( + (if (it[0] < 1.0f) 1.0f else it[0]) * scale, + (if (it[1] < 0.1f) 0.1f else it[1]) * scale + ), it[2] * scale) + } + } + if (sprite.frameEntity.maskPath !== null) canvas.save() + sprite.frameEntity.maskPath?.let { maskPath -> + val path2 = this.sharedValues.sharedPath2() + maskPath.buildPath(path2) + path2.transform(frameMatrix) + canvas.clipPath(path2) + } + canvas.drawPath(path, paint) + if (sprite.frameEntity.maskPath !== null) canvas.restore() + } + } + } + + } + } + + private val matrixScaleTempValues = FloatArray(16) + + private fun matrixScale(matrix: Matrix): Float { + matrix.getValues(matrixScaleTempValues) + if (matrixScaleTempValues[0] == 0f) { + return 0f + } + var A = matrixScaleTempValues[0].toDouble() + var B = matrixScaleTempValues[3].toDouble() + var C = matrixScaleTempValues[1].toDouble() + var D = matrixScaleTempValues[4].toDouble() + if (A * D == B * C) return 0f + var scaleX = Math.sqrt(A * A + B * B) + A /= scaleX + B /= scaleX + var skew = A * C + B * D + C -= A * skew + D -= B * skew + var scaleY = Math.sqrt(C * C + D * D) + C /= scaleY + D /= scaleY + skew /= scaleY + if (A * D < B * C) { + scaleX = -scaleX + } + return if (scaleInfo.ratioX) Math.abs(scaleX.toFloat()) else Math.abs(scaleY.toFloat()) + } + + private fun drawDynamic(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { + val imageKey = sprite.imageKey ?: return + dynamicItem.dynamicDrawer[imageKey]?.let { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + canvas.save() + canvas.concat(frameMatrix) + it.invoke(canvas, frameIndex) + canvas.restore() + } + dynamicItem.dynamicDrawerSized[imageKey]?.let { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + canvas.save() + canvas.concat(frameMatrix) + it.invoke(canvas, frameIndex, sprite.frameEntity.layout.width.toInt(), sprite.frameEntity.layout.height.toInt()) + canvas.restore() + } + } + + class ShareValues { + + private val sharedPaint = Paint() + private val sharedPath = Path() + private val sharedPath2 = Path() + private val sharedMatrix = Matrix() + private val sharedMatrix2 = Matrix() + + private val shareMattePaint = Paint() + private var shareMatteCanvas: Canvas? = null + private var sharedMatteBitmap: Bitmap? = null + + fun sharedPaint(): Paint { + sharedPaint.reset() + return sharedPaint + } + + fun sharedPath(): Path { + sharedPath.reset() + return sharedPath + } + + fun sharedPath2(): Path { + sharedPath2.reset() + return sharedPath2 + } + + fun sharedMatrix(): Matrix { + sharedMatrix.reset() + return sharedMatrix + } + + fun sharedMatrix2(): Matrix { + sharedMatrix2.reset() + return sharedMatrix2 + } + + fun shareMattePaint(): Paint { + shareMattePaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_IN)) + return shareMattePaint + } + + fun sharedMatteBitmap(): Bitmap { + return sharedMatteBitmap as Bitmap + } + + fun shareMatteCanvas(width: Int, height: Int): Canvas { + if (shareMatteCanvas == null) { + sharedMatteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8) +// shareMatteCanvas = Canvas(sharedMatteBitmap) + } +// val matteCanvas = shareMatteCanvas as Canvas +// matteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) +// return matteCanvas + return Canvas(sharedMatteBitmap!!) + } + } + + class PathCache { + + private var canvasWidth: Int = 0 + private var canvasHeight: Int = 0 + private val cache = HashMap() + + fun onSizeChanged(canvas: Canvas) { + if (this.canvasWidth != canvas.width || this.canvasHeight != canvas.height) { + this.cache.clear() + } + this.canvasWidth = canvas.width + this.canvasHeight = canvas.height + } + + fun buildPath(shape: SVGAVideoShapeEntity): Path { + if (!this.cache.containsKey(shape)) { + val path = Path() + path.set(shape.shapePath!!) + this.cache[shape] = path + } + return this.cache[shape]!! + } + + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt new file mode 100644 index 0000000..4788cc0 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt @@ -0,0 +1,24 @@ +package com.opensource.svgaplayer.entities + +import com.opensource.svgaplayer.proto.AudioEntity +import java.io.FileInputStream + +internal class SVGAAudioEntity { + + val audioKey: String? + val startFrame: Int + val endFrame: Int + val startTime: Int + val totalTime: Int + var soundID: Int? = null + var playID: Int? = null + + constructor(audioItem: AudioEntity) { + this.audioKey = audioItem.audioKey + this.startFrame = audioItem.startFrame ?: 0 + this.endFrame = audioItem.endFrame ?: 0 + this.startTime = audioItem.startTime ?: 0 + this.totalTime = audioItem.totalTime ?: 0 + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt new file mode 100644 index 0000000..d6f582c --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt @@ -0,0 +1,100 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Path +import com.opensource.svgaplayer.utils.SVGAPoint +import java.util.* + +private val VALID_METHODS: Set = setOf("M", "L", "H", "V", "C", "S", "Q", "R", "A", "Z", "m", "l", "h", "v", "c", "s", "q", "r", "a", "z") + +class SVGAPathEntity(originValue: String) { + + private val replacedValue: String = if (originValue.contains(",")) originValue.replace(",", " ") else originValue + + private var cachedPath: Path? = null + + fun buildPath(toPath: Path) { + cachedPath?.let { + toPath.set(it) + return + } + val cachedPath = Path() + val segments = StringTokenizer(this.replacedValue, "MLHVCSQRAZmlhvcsqraz", true) + var currentMethod = "" + while (segments.hasMoreTokens()) { + val segment = segments.nextToken() + if (segment.isEmpty()) { continue } + if (VALID_METHODS.contains(segment)) { + currentMethod = segment + if (currentMethod == "Z" || currentMethod == "z") { operate(cachedPath, currentMethod, StringTokenizer("", "")) } + } + else { + operate(cachedPath, currentMethod, StringTokenizer(segment, " ")) + } + } + this.cachedPath = cachedPath + toPath.set(cachedPath) + } + + private fun operate(finalPath: Path, method: String, args: StringTokenizer) { + var x0 = 0.0f + var y0 = 0.0f + var x1 = 0.0f + var y1 = 0.0f + var x2 = 0.0f + var y2 = 0.0f + try { + var index = 0 + while (args.hasMoreTokens()) { + val s = args.nextToken() + if (s.isEmpty()) {continue} + if (index == 0) { x0 = s.toFloat() } + if (index == 1) { y0 = s.toFloat() } + if (index == 2) { x1 = s.toFloat() } + if (index == 3) { y1 = s.toFloat() } + if (index == 4) { x2 = s.toFloat() } + if (index == 5) { y2 = s.toFloat() } + index++ + } + } catch (e: Exception) {} + var currentPoint = SVGAPoint(0.0f, 0.0f, 0.0f) + if (method == "M") { + finalPath.moveTo(x0, y0) + currentPoint = SVGAPoint(x0, y0, 0.0f) + } else if (method == "m") { + finalPath.rMoveTo(x0, y0) + currentPoint = SVGAPoint(currentPoint.x + x0, currentPoint.y + y0, 0.0f) + } + if (method == "L") { + finalPath.lineTo(x0, y0) + } else if (method == "l") { + finalPath.rLineTo(x0, y0) + } + if (method == "C") { + finalPath.cubicTo(x0, y0, x1, y1, x2, y2) + } else if (method == "c") { + finalPath.rCubicTo(x0, y0, x1, y1, x2, y2) + } + if (method == "Q") { + finalPath.quadTo(x0, y0, x1, y1) + } else if (method == "q") { + finalPath.rQuadTo(x0, y0, x1, y1) + } + if (method == "H") { + finalPath.lineTo(x0, currentPoint.y) + } else if (method == "h") { + finalPath.rLineTo(x0, 0f) + } + if (method == "V") { + finalPath.lineTo(currentPoint.x, x0) + } else if (method == "v") { + finalPath.rLineTo(0f, x0) + } + if (method == "Z") { + finalPath.close() + } + else if (method == "z") { + finalPath.close() + } + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt new file mode 100644 index 0000000..1f4cbb9 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt @@ -0,0 +1,356 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Path +import android.graphics.RectF +import com.opensource.svgaplayer.proto.ShapeEntity +import org.json.JSONArray +import org.json.JSONObject +import java.util.* + +/** + * Created by cuiminghui on 2017/2/22. + */ + +val sharedPath = Path() + +internal class SVGAVideoShapeEntity { + + enum class Type { + shape, + rect, + ellipse, + keep + } + + class Styles { + + var fill = 0x00000000 + internal set + + var stroke = 0x00000000 + internal set + + var strokeWidth = 0.0f + internal set + + var lineCap = "butt" + internal set + + var lineJoin = "miter" + internal set + + var miterLimit = 0 + internal set + + var lineDash = FloatArray(0) + internal set + + } + + var type = Type.shape + private set + + var args: Map? = null + private set + + var styles: Styles? = null + private set + + var transform: Matrix? = null + private set + + constructor(obj: JSONObject) { + parseType(obj) + parseArgs(obj) + parseStyles(obj) + parseTransform(obj) + } + + constructor(obj: ShapeEntity) { + parseType(obj) + parseArgs(obj) + parseStyles(obj) + parseTransform(obj) + } + + val isKeep: Boolean + get() = type == Type.keep + + var shapePath: Path? = null + + private fun parseType(obj: JSONObject) { + obj.optString("type")?.let { + when { + it.equals("shape", ignoreCase = true) -> type = Type.shape + it.equals("rect", ignoreCase = true) -> type = Type.rect + it.equals("ellipse", ignoreCase = true) -> type = Type.ellipse + it.equals("keep", ignoreCase = true) -> type = Type.keep + } + } + } + + private fun parseType(obj: ShapeEntity) { + obj.type?.let { + type = when (it) { + ShapeEntity.ShapeType.SHAPE -> Type.shape + ShapeEntity.ShapeType.RECT -> Type.rect + ShapeEntity.ShapeType.ELLIPSE -> Type.ellipse + ShapeEntity.ShapeType.KEEP -> Type.keep + } + } + } + + private fun parseArgs(obj: JSONObject) { + val args = HashMap() + obj.optJSONObject("args")?.let { values -> + values.keys().forEach { key -> + values.get(key)?.let { + args.put(key, it) + } + } + this.args = args + } + } + + private fun parseArgs(obj: ShapeEntity) { + val args = HashMap() + obj.shape?.let { + it.d?.let { args.put("d", it) } + } + obj.ellipse?.let { + args.put("x", it.x ?: 0.0f) + args.put("y", it.y ?: 0.0f) + args.put("radiusX", it.radiusX ?: 0.0f) + args.put("radiusY", it.radiusY ?: 0.0f) + } + obj.rect?.let { + args.put("x", it.x ?: 0.0f) + args.put("y", it.y ?: 0.0f) + args.put("width", it.width ?: 0.0f) + args.put("height", it.height ?: 0.0f) + args.put("cornerRadius", it.cornerRadius ?: 0.0f) + } + this.args = args + } + + // 检查色域范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkValueRange(obj: JSONArray): Float { + return if ( + obj.optDouble(0) <= 1 && + obj.optDouble(1) <= 1 && + obj.optDouble(2) <= 1 + ) { + 255f + } else { + 1f + } + } + + // 检查 alpha 的范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkAlphaValueRange(obj: JSONArray): Float { + return if (obj.optDouble(3) <= 1) { + 255f + } else { + 1f + } + } + + private fun parseStyles(obj: JSONObject) { + obj.optJSONObject("styles")?.let { + val styles = Styles() + it.optJSONArray("fill")?.let { + if (it.length() == 4) { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.fill = Color.argb( + (it.optDouble(3) * alphaRangeValue).toInt(), + (it.optDouble(0) * mulValue).toInt(), + (it.optDouble(1) * mulValue).toInt(), + (it.optDouble(2) * mulValue).toInt() + ) + } + } + it.optJSONArray("stroke")?.let { + if (it.length() == 4) { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.stroke = Color.argb( + (it.optDouble(3) * alphaRangeValue).toInt(), + (it.optDouble(0) * mulValue).toInt(), + (it.optDouble(1) * mulValue).toInt(), + (it.optDouble(2) * mulValue).toInt() + ) + } + } + styles.strokeWidth = it.optDouble("strokeWidth", 0.0).toFloat() + styles.lineCap = it.optString("lineCap", "butt") + styles.lineJoin = it.optString("lineJoin", "miter") + styles.miterLimit = it.optInt("miterLimit", 0) + it.optJSONArray("lineDash")?.let { + styles.lineDash = FloatArray(it.length()) + for (i in 0 until it.length()) { + styles.lineDash[i] = it.optDouble(i, 0.0).toFloat() + } + } + this.styles = styles + } + } + + // 检查色域范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkValueRange(color: ShapeEntity.ShapeStyle.RGBAColor): Float { + return if ( + (color.r ?: 0f) <= 1 && + (color.g ?: 0f) <= 1 && + (color.b ?: 0f) <= 1 + ) { + 255f + } else { + 1f + } + } + + // 检查 alpha 范围是否是 [0f, 1f],有可能是 [0f, 255f] + private fun checkAlphaValueRange(color: ShapeEntity.ShapeStyle.RGBAColor): Float { + return if (color.a <= 1f) { + 255f + } else { + 1f + } + } + + private fun parseStyles(obj: ShapeEntity) { + obj.styles?.let { + val styles = Styles() + it.fill?.let { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.fill = Color.argb( + ((it.a ?: 0f) * alphaRangeValue).toInt(), + ((it.r ?: 0f) * mulValue).toInt(), + ((it.g ?: 0f) * mulValue).toInt(), + ((it.b ?: 0f) * mulValue).toInt() + ) + } + it.stroke?.let { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.stroke = Color.argb( + ((it.a ?: 0f) * alphaRangeValue).toInt(), + ((it.r ?: 0f) * mulValue).toInt(), + ((it.g ?: 0f) * mulValue).toInt(), + ((it.b ?: 0f) * mulValue).toInt() + ) + + } + styles.strokeWidth = it.strokeWidth ?: 0.0f + it.lineCap?.let { + when (it) { + ShapeEntity.ShapeStyle.LineCap.LineCap_BUTT -> styles.lineCap = "butt" + ShapeEntity.ShapeStyle.LineCap.LineCap_ROUND -> styles.lineCap = "round" + ShapeEntity.ShapeStyle.LineCap.LineCap_SQUARE -> styles.lineCap = "square" + } + } + it.lineJoin?.let { + when (it) { + ShapeEntity.ShapeStyle.LineJoin.LineJoin_BEVEL -> styles.lineJoin = "bevel" + ShapeEntity.ShapeStyle.LineJoin.LineJoin_MITER -> styles.lineJoin = "miter" + ShapeEntity.ShapeStyle.LineJoin.LineJoin_ROUND -> styles.lineJoin = "round" + } + } + styles.miterLimit = (it.miterLimit ?: 0.0f).toInt() + styles.lineDash = kotlin.FloatArray(3) + it.lineDashI?.let { styles.lineDash[0] = it } + it.lineDashII?.let { styles.lineDash[1] = it } + it.lineDashIII?.let { styles.lineDash[2] = it } + this.styles = styles + } + } + + private fun parseTransform(obj: JSONObject) { + obj.optJSONObject("transform")?.let { + val transform = Matrix() + val arr = FloatArray(9) + val a = it.optDouble("a", 1.0) + val b = it.optDouble("b", 0.0) + val c = it.optDouble("c", 0.0) + val d = it.optDouble("d", 1.0) + val tx = it.optDouble("tx", 0.0) + val ty = it.optDouble("ty", 0.0) + arr[0] = a.toFloat() // a + arr[1] = c.toFloat() // c + arr[2] = tx.toFloat() // tx + arr[3] = b.toFloat() // b + arr[4] = d.toFloat() // d + arr[5] = ty.toFloat() // ty + arr[6] = 0.0.toFloat() + arr[7] = 0.0.toFloat() + arr[8] = 1.0.toFloat() + transform.setValues(arr) + this.transform = transform + } + } + + private fun parseTransform(obj: ShapeEntity) { + obj.transform?.let { + val transform = Matrix() + val arr = FloatArray(9) + val a = it.a ?: 1.0f + val b = it.b ?: 0.0f + val c = it.c ?: 0.0f + val d = it.d ?: 1.0f + val tx = it.tx ?: 0.0f + val ty = it.ty ?: 0.0f + arr[0] = a + arr[1] = c + arr[2] = tx + arr[3] = b + arr[4] = d + arr[5] = ty + arr[6] = 0.0f + arr[7] = 0.0f + arr[8] = 1.0f + transform.setValues(arr) + this.transform = transform + } + } + + + fun buildPath() { + if (this.shapePath != null) { + return + } + sharedPath.reset() + if (this.type == Type.shape) { + (this.args?.get("d") as? String)?.let { + SVGAPathEntity(it).buildPath(sharedPath) + } + } else if (this.type == Type.ellipse) { + val xv = this.args?.get("x") as? Number ?: return + val yv = this.args?.get("y") as? Number ?: return + val rxv = this.args?.get("radiusX") as? Number ?: return + val ryv = this.args?.get("radiusY") as? Number ?: return + val x = xv.toFloat() + val y = yv.toFloat() + val rx = rxv.toFloat() + val ry = ryv.toFloat() + sharedPath.addOval(RectF(x - rx, y - ry, x + rx, y + ry), Path.Direction.CW) + } else if (this.type == Type.rect) { + val xv = this.args?.get("x") as? Number ?: return + val yv = this.args?.get("y") as? Number ?: return + val wv = this.args?.get("width") as? Number ?: return + val hv = this.args?.get("height") as? Number ?: return + val crv = this.args?.get("cornerRadius") as? Number ?: return + val x = xv.toFloat() + val y = yv.toFloat() + val width = wv.toFloat() + val height = hv.toFloat() + val cornerRadius = crv.toFloat() + sharedPath.addRoundRect(RectF(x, y, x + width, y + height), cornerRadius, cornerRadius, Path.Direction.CW) + } + this.shapePath = Path() + this.shapePath?.set(sharedPath) + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt new file mode 100644 index 0000000..6e9fbc2 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt @@ -0,0 +1,60 @@ +package com.opensource.svgaplayer.entities + +import com.opensource.svgaplayer.proto.SpriteEntity +import org.json.JSONObject + +/** + * Created by cuiminghui on 2016/10/17. + */ +internal class SVGAVideoSpriteEntity { + + val imageKey: String? + + val matteKey: String? + + val frames: List + + constructor(obj: JSONObject) { + this.imageKey = obj.optString("imageKey") + this.matteKey = obj.optString("matteKey") + val mutableFrames: MutableList = mutableListOf() + obj.optJSONArray("frames")?.let { + for (i in 0 until it.length()) { + it.optJSONObject(i)?.let { + val frameItem = SVGAVideoSpriteFrameEntity(it) + if (frameItem.shapes.isNotEmpty()) { + frameItem.shapes.first().let { + if (it.isKeep && mutableFrames.size > 0) { + frameItem.shapes = mutableFrames.last().shapes + } + } + } + mutableFrames.add(frameItem) + } + } + } + frames = mutableFrames.toList() + } + + constructor(obj: SpriteEntity) { + this.imageKey = obj.imageKey + this.matteKey = obj.matteKey + var lastFrame: SVGAVideoSpriteFrameEntity? = null + frames = obj.frames?.map { + val frameItem = SVGAVideoSpriteFrameEntity(it) + if (frameItem.shapes.isNotEmpty()) { + frameItem.shapes.first().let { + if (it.isKeep) { + lastFrame?.let { + frameItem.shapes = it.shapes + } + } + } + } + lastFrame = frameItem + return@map frameItem + } ?: listOf() + + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt new file mode 100644 index 0000000..078d91a --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt @@ -0,0 +1,94 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Matrix +import com.opensource.svgaplayer.proto.FrameEntity +import com.opensource.svgaplayer.utils.SVGARect + +import org.json.JSONObject + +/** + * Created by cuiminghui on 2016/10/17. + */ +internal class SVGAVideoSpriteFrameEntity { + + var alpha: Double + var layout = SVGARect(0.0, 0.0, 0.0, 0.0) + var transform = Matrix() + var maskPath: SVGAPathEntity? = null + var shapes: List = listOf() + + constructor(obj: JSONObject) { + this.alpha = obj.optDouble("alpha", 0.0) + obj.optJSONObject("layout")?.let { + layout = SVGARect(it.optDouble("x", 0.0), it.optDouble("y", 0.0), it.optDouble("width", 0.0), it.optDouble("height", 0.0)) + } + obj.optJSONObject("transform")?.let { + val arr = FloatArray(9) + val a = it.optDouble("a", 1.0) + val b = it.optDouble("b", 0.0) + val c = it.optDouble("c", 0.0) + val d = it.optDouble("d", 1.0) + val tx = it.optDouble("tx", 0.0) + val ty = it.optDouble("ty", 0.0) + arr[0] = a.toFloat() + arr[1] = c.toFloat() + arr[2] = tx.toFloat() + arr[3] = b.toFloat() + arr[4] = d.toFloat() + arr[5] = ty.toFloat() + arr[6] = 0.0.toFloat() + arr[7] = 0.0.toFloat() + arr[8] = 1.0.toFloat() + transform.setValues(arr) + } + obj.optString("clipPath")?.let { d -> + if (d.isNotEmpty()) { + maskPath = SVGAPathEntity(d) + } + } + obj.optJSONArray("shapes")?.let { + val mutableList: MutableList = mutableListOf() + for (i in 0 until it.length()) { + it.optJSONObject(i)?.let { + mutableList.add(SVGAVideoShapeEntity(it)) + } + } + shapes = mutableList.toList() + } + } + + constructor(obj: FrameEntity) { + this.alpha = (obj.alpha ?: 0.0f).toDouble() + obj.layout?.let { + this.layout = SVGARect((it.x ?: 0.0f).toDouble(), (it.y + ?: 0.0f).toDouble(), (it.width ?: 0.0f).toDouble(), (it.height + ?: 0.0f).toDouble()) + } + obj.transform?.let { + val arr = FloatArray(9) + val a = it.a ?: 1.0f + val b = it.b ?: 0.0f + val c = it.c ?: 0.0f + val d = it.d ?: 1.0f + val tx = it.tx ?: 0.0f + val ty = it.ty ?: 0.0f + arr[0] = a + arr[1] = c + arr[2] = tx + arr[3] = b + arr[4] = d + arr[5] = ty + arr[6] = 0.0f + arr[7] = 0.0f + arr[8] = 1.0f + transform.setValues(arr) + } + obj.clipPath?.takeIf { it.isNotEmpty() }?.let { + maskPath = SVGAPathEntity(it) + } + this.shapes = obj.shapes.map { + return@map SVGAVideoShapeEntity(it) + } + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java b/library/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java new file mode 100644 index 0000000..531e949 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java @@ -0,0 +1,255 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 19:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; + +import okio.ByteString; + +public final class AudioEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_AudioEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_AUDIOKEY = ""; + + public static final Integer DEFAULT_STARTFRAME = 0; + + public static final Integer DEFAULT_ENDFRAME = 0; + + public static final Integer DEFAULT_STARTTIME = 0; + + public static final Integer DEFAULT_TOTALTIME = 0; + + /** + * 音频文件名 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String audioKey; + + /** + * 音频播放起始帧 + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer startFrame; + + /** + * 音频播放结束帧 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer endFrame; + + /** + * 音频播放起始时间(相对音频长度) + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer startTime; + + /** + * 音频总长度 + */ + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer totalTime; + + public AudioEntity(String audioKey, Integer startFrame, Integer endFrame, Integer startTime, Integer totalTime) { + this(audioKey, startFrame, endFrame, startTime, totalTime, ByteString.EMPTY); + } + + public AudioEntity(String audioKey, Integer startFrame, Integer endFrame, Integer startTime, Integer totalTime, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.audioKey = audioKey; + this.startFrame = startFrame; + this.endFrame = endFrame; + this.startTime = startTime; + this.totalTime = totalTime; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.audioKey = audioKey; + builder.startFrame = startFrame; + builder.endFrame = endFrame; + builder.startTime = startTime; + builder.totalTime = totalTime; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof AudioEntity)) return false; + AudioEntity o = (AudioEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(audioKey, o.audioKey) + && Internal.equals(startFrame, o.startFrame) + && Internal.equals(endFrame, o.endFrame) + && Internal.equals(startTime, o.startTime) + && Internal.equals(totalTime, o.totalTime); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (audioKey != null ? audioKey.hashCode() : 0); + result = result * 37 + (startFrame != null ? startFrame.hashCode() : 0); + result = result * 37 + (endFrame != null ? endFrame.hashCode() : 0); + result = result * 37 + (startTime != null ? startTime.hashCode() : 0); + result = result * 37 + (totalTime != null ? totalTime.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (audioKey != null) builder.append(", audioKey=").append(audioKey); + if (startFrame != null) builder.append(", startFrame=").append(startFrame); + if (endFrame != null) builder.append(", endFrame=").append(endFrame); + if (startTime != null) builder.append(", startTime=").append(startTime); + if (totalTime != null) builder.append(", totalTime=").append(totalTime); + return builder.replace(0, 2, "AudioEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String audioKey; + + public Integer startFrame; + + public Integer endFrame; + + public Integer startTime; + + public Integer totalTime; + + public Builder() { + } + + /** + * 音频文件名 + */ + public Builder audioKey(String audioKey) { + this.audioKey = audioKey; + return this; + } + + /** + * 音频播放起始帧 + */ + public Builder startFrame(Integer startFrame) { + this.startFrame = startFrame; + return this; + } + + /** + * 音频播放结束帧 + */ + public Builder endFrame(Integer endFrame) { + this.endFrame = endFrame; + return this; + } + + /** + * 音频播放起始时间(相对音频长度) + */ + public Builder startTime(Integer startTime) { + this.startTime = startTime; + return this; + } + + /** + * 音频总长度 + */ + public Builder totalTime(Integer totalTime) { + this.totalTime = totalTime; + return this; + } + + @Override + public AudioEntity build() { + return new AudioEntity(audioKey, startFrame, endFrame, startTime, totalTime, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_AudioEntity extends ProtoAdapter { + ProtoAdapter_AudioEntity() { + super(FieldEncoding.LENGTH_DELIMITED, AudioEntity.class); + } + + @Override + public int encodedSize(AudioEntity value) { + return (value.audioKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.audioKey) : 0) + + (value.startFrame != null ? ProtoAdapter.INT32.encodedSizeWithTag(2, value.startFrame) : 0) + + (value.endFrame != null ? ProtoAdapter.INT32.encodedSizeWithTag(3, value.endFrame) : 0) + + (value.startTime != null ? ProtoAdapter.INT32.encodedSizeWithTag(4, value.startTime) : 0) + + (value.totalTime != null ? ProtoAdapter.INT32.encodedSizeWithTag(5, value.totalTime) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, AudioEntity value) throws IOException { + if (value.audioKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.audioKey); + if (value.startFrame != null) ProtoAdapter.INT32.encodeWithTag(writer, 2, value.startFrame); + if (value.endFrame != null) ProtoAdapter.INT32.encodeWithTag(writer, 3, value.endFrame); + if (value.startTime != null) ProtoAdapter.INT32.encodeWithTag(writer, 4, value.startTime); + if (value.totalTime != null) ProtoAdapter.INT32.encodeWithTag(writer, 5, value.totalTime); + writer.writeBytes(value.unknownFields()); + } + + @Override + public AudioEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.audioKey(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.startFrame(ProtoAdapter.INT32.decode(reader)); break; + case 3: builder.endFrame(ProtoAdapter.INT32.decode(reader)); break; + case 4: builder.startTime(ProtoAdapter.INT32.decode(reader)); break; + case 5: builder.totalTime(ProtoAdapter.INT32.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public AudioEntity redact(AudioEntity value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java b/library/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java new file mode 100644 index 0000000..82ea2e4 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java @@ -0,0 +1,256 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 115:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; +import java.util.List; + +import okio.ByteString; + +public final class FrameEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_FrameEntity(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_ALPHA = 0.0f; + + public static final String DEFAULT_CLIPPATH = ""; + + /** + * 透明度 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float alpha; + + /** + * 初始约束大小 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.Layout#ADAPTER" + ) + public final com.opensource.svgaplayer.proto.Layout layout; + + /** + * 2D 变换矩阵 + */ + @WireField( + tag = 3, + adapter = "com.opensource.svgaplayer.proto.Transform#ADAPTER" + ) + public final com.opensource.svgaplayer.proto.Transform transform; + + /** + * 遮罩路径,使用 SVG 标准 Path 绘制图案进行 Mask 遮罩。 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String clipPath; + + /** + * 矢量元素列表 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List shapes; + + public FrameEntity(Float alpha, com.opensource.svgaplayer.proto.Layout layout, com.opensource.svgaplayer.proto.Transform transform, String clipPath, List shapes) { + this(alpha, layout, transform, clipPath, shapes, ByteString.EMPTY); + } + + public FrameEntity(Float alpha, com.opensource.svgaplayer.proto.Layout layout, com.opensource.svgaplayer.proto.Transform transform, String clipPath, List shapes, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.alpha = alpha; + this.layout = layout; + this.transform = transform; + this.clipPath = clipPath; + this.shapes = Internal.immutableCopyOf("shapes", shapes); + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.alpha = alpha; + builder.layout = layout; + builder.transform = transform; + builder.clipPath = clipPath; + builder.shapes = Internal.copyOf("shapes", shapes); + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof FrameEntity)) return false; + FrameEntity o = (FrameEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(alpha, o.alpha) + && Internal.equals(layout, o.layout) + && Internal.equals(transform, o.transform) + && Internal.equals(clipPath, o.clipPath) + && shapes.equals(o.shapes); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (alpha != null ? alpha.hashCode() : 0); + result = result * 37 + (layout != null ? layout.hashCode() : 0); + result = result * 37 + (transform != null ? transform.hashCode() : 0); + result = result * 37 + (clipPath != null ? clipPath.hashCode() : 0); + result = result * 37 + shapes.hashCode(); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (alpha != null) builder.append(", alpha=").append(alpha); + if (layout != null) builder.append(", layout=").append(layout); + if (transform != null) builder.append(", transform=").append(transform); + if (clipPath != null) builder.append(", clipPath=").append(clipPath); + if (!shapes.isEmpty()) builder.append(", shapes=").append(shapes); + return builder.replace(0, 2, "FrameEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float alpha; + + public com.opensource.svgaplayer.proto.Layout layout; + + public com.opensource.svgaplayer.proto.Transform transform; + + public String clipPath; + + public List shapes; + + public Builder() { + shapes = Internal.newMutableList(); + } + + /** + * 透明度 + */ + public Builder alpha(Float alpha) { + this.alpha = alpha; + return this; + } + + /** + * 初始约束大小 + */ + public Builder layout(com.opensource.svgaplayer.proto.Layout layout) { + this.layout = layout; + return this; + } + + /** + * 2D 变换矩阵 + */ + public Builder transform(com.opensource.svgaplayer.proto.Transform transform) { + this.transform = transform; + return this; + } + + /** + * 遮罩路径,使用 SVG 标准 Path 绘制图案进行 Mask 遮罩。 + */ + public Builder clipPath(String clipPath) { + this.clipPath = clipPath; + return this; + } + + /** + * 矢量元素列表 + */ + public Builder shapes(List shapes) { + Internal.checkElementsNotNull(shapes); + this.shapes = shapes; + return this; + } + + @Override + public FrameEntity build() { + return new FrameEntity(alpha, layout, transform, clipPath, shapes, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_FrameEntity extends ProtoAdapter { + ProtoAdapter_FrameEntity() { + super(FieldEncoding.LENGTH_DELIMITED, FrameEntity.class); + } + + @Override + public int encodedSize(FrameEntity value) { + return (value.alpha != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.alpha) : 0) + + (value.layout != null ? com.opensource.svgaplayer.proto.Layout.ADAPTER.encodedSizeWithTag(2, value.layout) : 0) + + (value.transform != null ? com.opensource.svgaplayer.proto.Transform.ADAPTER.encodedSizeWithTag(3, value.transform) : 0) + + (value.clipPath != null ? ProtoAdapter.STRING.encodedSizeWithTag(4, value.clipPath) : 0) + + ShapeEntity.ADAPTER.asRepeated().encodedSizeWithTag(5, value.shapes) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, FrameEntity value) throws IOException { + if (value.alpha != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.alpha); + if (value.layout != null) com.opensource.svgaplayer.proto.Layout.ADAPTER.encodeWithTag(writer, 2, value.layout); + if (value.transform != null) com.opensource.svgaplayer.proto.Transform.ADAPTER.encodeWithTag(writer, 3, value.transform); + if (value.clipPath != null) ProtoAdapter.STRING.encodeWithTag(writer, 4, value.clipPath); + ShapeEntity.ADAPTER.asRepeated().encodeWithTag(writer, 5, value.shapes); + writer.writeBytes(value.unknownFields()); + } + + @Override + public FrameEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.alpha(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.layout(com.opensource.svgaplayer.proto.Layout.ADAPTER.decode(reader)); break; + case 3: builder.transform(com.opensource.svgaplayer.proto.Transform.ADAPTER.decode(reader)); break; + case 4: builder.clipPath(ProtoAdapter.STRING.decode(reader)); break; + case 5: builder.shapes.add(ShapeEntity.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public FrameEntity redact(FrameEntity value) { + Builder builder = value.newBuilder(); + if (builder.layout != null) builder.layout = Layout.ADAPTER.redact(builder.layout); + if (builder.transform != null) builder.transform = Transform.ADAPTER.redact(builder.transform); + Internal.redactElements(builder.shapes, ShapeEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/Layout.java b/library/src/main/java/com/opensource/svgaplayer/proto/Layout.java new file mode 100644 index 0000000..7e179bb --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/Layout.java @@ -0,0 +1,202 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 27:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; + +import okio.ByteString; + +public final class Layout extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_Layout(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_WIDTH = 0.0f; + + public static final Float DEFAULT_HEIGHT = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float width; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float height; + + public Layout(Float x, Float y, Float width, Float height) { + this(x, y, width, height, ByteString.EMPTY); + } + + public Layout(Float x, Float y, Float width, Float height, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.width = width; + builder.height = height; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof Layout)) return false; + Layout o = (Layout) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(width, o.width) + && Internal.equals(height, o.height); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (width != null ? width.hashCode() : 0); + result = result * 37 + (height != null ? height.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (width != null) builder.append(", width=").append(width); + if (height != null) builder.append(", height=").append(height); + return builder.replace(0, 2, "Layout{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float width; + + public Float height; + + public Builder() { + } + + public Builder x(Float x) { + this.x = x; + return this; + } + + public Builder y(Float y) { + this.y = y; + return this; + } + + public Builder width(Float width) { + this.width = width; + return this; + } + + public Builder height(Float height) { + this.height = height; + return this; + } + + @Override + public Layout build() { + return new Layout(x, y, width, height, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_Layout extends ProtoAdapter { + ProtoAdapter_Layout() { + super(FieldEncoding.LENGTH_DELIMITED, Layout.class); + } + + @Override + public int encodedSize(Layout value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.width != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.width) : 0) + + (value.height != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.height) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, Layout value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.width != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.width); + if (value.height != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.height); + writer.writeBytes(value.unknownFields()); + } + + @Override + public Layout decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.width(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.height(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public Layout redact(Layout value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java b/library/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java new file mode 100644 index 0000000..7e7b3d0 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java @@ -0,0 +1,263 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 123:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import okio.ByteString; + +public final class MovieEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_MovieEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_VERSION = ""; + + /** + * SVGA 格式版本号 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String version; + + /** + * 动画参数 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.MovieParams#ADAPTER" + ) + public final MovieParams params; + + /** + * Key 是位图键名,Value 是位图文件名或二进制 PNG 数据。 + */ + @WireField( + tag = 3, + keyAdapter = "com.squareup.wire.ProtoAdapter#STRING", + adapter = "com.squareup.wire.ProtoAdapter#BYTES" + ) + public final Map images; + + /** + * 元素列表 + */ + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.SpriteEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List sprites; + + /** + * 音频列表 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.AudioEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List audios; + + public MovieEntity(String version, MovieParams params, Map images, List sprites, List audios) { + this(version, params, images, sprites, audios, ByteString.EMPTY); + } + + public MovieEntity(String version, MovieParams params, Map images, List sprites, List audios, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.version = version; + this.params = params; + this.images = Internal.immutableCopyOf("images", images); + this.sprites = Internal.immutableCopyOf("sprites", sprites); + this.audios = Internal.immutableCopyOf("audios", audios); + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.version = version; + builder.params = params; + builder.images = Internal.copyOf("images", images); + builder.sprites = Internal.copyOf("sprites", sprites); + builder.audios = Internal.copyOf("audios", audios); + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof MovieEntity)) return false; + MovieEntity o = (MovieEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(version, o.version) + && Internal.equals(params, o.params) + && images.equals(o.images) + && sprites.equals(o.sprites) + && audios.equals(o.audios); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (version != null ? version.hashCode() : 0); + result = result * 37 + (params != null ? params.hashCode() : 0); + result = result * 37 + images.hashCode(); + result = result * 37 + sprites.hashCode(); + result = result * 37 + audios.hashCode(); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (version != null) builder.append(", version=").append(version); + if (params != null) builder.append(", params=").append(params); + if (!images.isEmpty()) builder.append(", images=").append(images); + if (!sprites.isEmpty()) builder.append(", sprites=").append(sprites); + if (!audios.isEmpty()) builder.append(", audios=").append(audios); + return builder.replace(0, 2, "MovieEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String version; + + public MovieParams params; + + public Map images; + + public List sprites; + + public List audios; + + public Builder() { + images = Internal.newMutableMap(); + sprites = Internal.newMutableList(); + audios = Internal.newMutableList(); + } + + /** + * SVGA 格式版本号 + */ + public Builder version(String version) { + this.version = version; + return this; + } + + /** + * 动画参数 + */ + public Builder params(MovieParams params) { + this.params = params; + return this; + } + + /** + * Key 是位图键名,Value 是位图文件名或二进制 PNG 数据。 + */ + public Builder images(Map images) { + Internal.checkElementsNotNull(images); + this.images = images; + return this; + } + + /** + * 元素列表 + */ + public Builder sprites(List sprites) { + Internal.checkElementsNotNull(sprites); + this.sprites = sprites; + return this; + } + + /** + * 音频列表 + */ + public Builder audios(List audios) { + Internal.checkElementsNotNull(audios); + this.audios = audios; + return this; + } + + @Override + public MovieEntity build() { + return new MovieEntity(version, params, images, sprites, audios, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_MovieEntity extends ProtoAdapter { + private final ProtoAdapter> images = ProtoAdapter.newMapAdapter(ProtoAdapter.STRING, ProtoAdapter.BYTES); + + ProtoAdapter_MovieEntity() { + super(FieldEncoding.LENGTH_DELIMITED, MovieEntity.class); + } + + @Override + public int encodedSize(MovieEntity value) { + return (value.version != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.version) : 0) + + (value.params != null ? MovieParams.ADAPTER.encodedSizeWithTag(2, value.params) : 0) + + images.encodedSizeWithTag(3, value.images) + + SpriteEntity.ADAPTER.asRepeated().encodedSizeWithTag(4, value.sprites) + + com.opensource.svgaplayer.proto.AudioEntity.ADAPTER.asRepeated().encodedSizeWithTag(5, value.audios) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, MovieEntity value) throws IOException { + if (value.version != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.version); + if (value.params != null) MovieParams.ADAPTER.encodeWithTag(writer, 2, value.params); + images.encodeWithTag(writer, 3, value.images); + SpriteEntity.ADAPTER.asRepeated().encodeWithTag(writer, 4, value.sprites); + com.opensource.svgaplayer.proto.AudioEntity.ADAPTER.asRepeated().encodeWithTag(writer, 5, value.audios); + writer.writeBytes(value.unknownFields()); + } + + @Override + public MovieEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.version(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.params(MovieParams.ADAPTER.decode(reader)); break; + case 3: builder.images.putAll(images.decode(reader)); break; + case 4: builder.sprites.add(SpriteEntity.ADAPTER.decode(reader)); break; + case 5: builder.audios.add(com.opensource.svgaplayer.proto.AudioEntity.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public MovieEntity redact(MovieEntity value) { + Builder builder = value.newBuilder(); + if (builder.params != null) builder.params = MovieParams.ADAPTER.redact(builder.params); + Internal.redactElements(builder.sprites, SpriteEntity.ADAPTER); + Internal.redactElements(builder.audios, AudioEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java b/library/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java new file mode 100644 index 0000000..f851443 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java @@ -0,0 +1,226 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 6:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; + +import okio.ByteString; + +public final class MovieParams extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_MovieParams(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_VIEWBOXWIDTH = 0.0f; + + public static final Float DEFAULT_VIEWBOXHEIGHT = 0.0f; + + public static final Integer DEFAULT_FPS = 0; + + public static final Integer DEFAULT_FRAMES = 0; + + /** + * 画布宽 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float viewBoxWidth; + + /** + * 画布高 + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float viewBoxHeight; + + /** + * 动画每秒播放帧数,合法值是 [1, 2, 3, 5, 6, 10, 12, 15, 20, 30, 60] 中的任意一个。 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer fps; + + /** + * 动画总帧数 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer frames; + + public MovieParams(Float viewBoxWidth, Float viewBoxHeight, Integer fps, Integer frames) { + this(viewBoxWidth, viewBoxHeight, fps, frames, ByteString.EMPTY); + } + + public MovieParams(Float viewBoxWidth, Float viewBoxHeight, Integer fps, Integer frames, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.viewBoxWidth = viewBoxWidth; + this.viewBoxHeight = viewBoxHeight; + this.fps = fps; + this.frames = frames; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.viewBoxWidth = viewBoxWidth; + builder.viewBoxHeight = viewBoxHeight; + builder.fps = fps; + builder.frames = frames; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof MovieParams)) return false; + MovieParams o = (MovieParams) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(viewBoxWidth, o.viewBoxWidth) + && Internal.equals(viewBoxHeight, o.viewBoxHeight) + && Internal.equals(fps, o.fps) + && Internal.equals(frames, o.frames); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (viewBoxWidth != null ? viewBoxWidth.hashCode() : 0); + result = result * 37 + (viewBoxHeight != null ? viewBoxHeight.hashCode() : 0); + result = result * 37 + (fps != null ? fps.hashCode() : 0); + result = result * 37 + (frames != null ? frames.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (viewBoxWidth != null) builder.append(", viewBoxWidth=").append(viewBoxWidth); + if (viewBoxHeight != null) builder.append(", viewBoxHeight=").append(viewBoxHeight); + if (fps != null) builder.append(", fps=").append(fps); + if (frames != null) builder.append(", frames=").append(frames); + return builder.replace(0, 2, "MovieParams{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float viewBoxWidth; + + public Float viewBoxHeight; + + public Integer fps; + + public Integer frames; + + public Builder() { + } + + /** + * 画布宽 + */ + public Builder viewBoxWidth(Float viewBoxWidth) { + this.viewBoxWidth = viewBoxWidth; + return this; + } + + /** + * 画布高 + */ + public Builder viewBoxHeight(Float viewBoxHeight) { + this.viewBoxHeight = viewBoxHeight; + return this; + } + + /** + * 动画每秒播放帧数,合法值是 [1, 2, 3, 5, 6, 10, 12, 15, 20, 30, 60] 中的任意一个。 + */ + public Builder fps(Integer fps) { + this.fps = fps; + return this; + } + + /** + * 动画总帧数 + */ + public Builder frames(Integer frames) { + this.frames = frames; + return this; + } + + @Override + public MovieParams build() { + return new MovieParams(viewBoxWidth, viewBoxHeight, fps, frames, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_MovieParams extends ProtoAdapter { + ProtoAdapter_MovieParams() { + super(FieldEncoding.LENGTH_DELIMITED, MovieParams.class); + } + + @Override + public int encodedSize(MovieParams value) { + return (value.viewBoxWidth != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.viewBoxWidth) : 0) + + (value.viewBoxHeight != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.viewBoxHeight) : 0) + + (value.fps != null ? ProtoAdapter.INT32.encodedSizeWithTag(3, value.fps) : 0) + + (value.frames != null ? ProtoAdapter.INT32.encodedSizeWithTag(4, value.frames) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, MovieParams value) throws IOException { + if (value.viewBoxWidth != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.viewBoxWidth); + if (value.viewBoxHeight != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.viewBoxHeight); + if (value.fps != null) ProtoAdapter.INT32.encodeWithTag(writer, 3, value.fps); + if (value.frames != null) ProtoAdapter.INT32.encodeWithTag(writer, 4, value.frames); + writer.writeBytes(value.unknownFields()); + } + + @Override + public MovieParams decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.viewBoxWidth(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.viewBoxHeight(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.fps(ProtoAdapter.INT32.decode(reader)); break; + case 4: builder.frames(ProtoAdapter.INT32.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public MovieParams redact(MovieParams value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java b/library/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java new file mode 100644 index 0000000..b9783e6 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java @@ -0,0 +1,1500 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 43:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireEnum; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; + +import okio.ByteString; + +public final class ShapeEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeEntity(); + + private static final long serialVersionUID = 0L; + + public static final ShapeType DEFAULT_TYPE = ShapeType.SHAPE; + + /** + * 矢量类型 + */ + @WireField( + tag = 1, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeType#ADAPTER" + ) + public final ShapeType type; + + /** + * 矢量参数 + * 渲染参数 + */ + @WireField( + tag = 10, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle#ADAPTER" + ) + public final ShapeStyle styles; + + /** + * 矢量图层 2D 变换矩阵 + */ + @WireField( + tag = 11, + adapter = "com.opensource.svgaplayer.proto.Transform#ADAPTER" + ) + public final com.opensource.svgaplayer.proto.Transform transform; + + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeArgs#ADAPTER" + ) + public final ShapeArgs shape; + + @WireField( + tag = 3, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$RectArgs#ADAPTER" + ) + public final RectArgs rect; + + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$EllipseArgs#ADAPTER" + ) + public final EllipseArgs ellipse; + + public ShapeEntity(ShapeType type, ShapeStyle styles, com.opensource.svgaplayer.proto.Transform transform, ShapeArgs shape, RectArgs rect, EllipseArgs ellipse) { + this(type, styles, transform, shape, rect, ellipse, ByteString.EMPTY); + } + + public ShapeEntity(ShapeType type, ShapeStyle styles, com.opensource.svgaplayer.proto.Transform transform, ShapeArgs shape, RectArgs rect, EllipseArgs ellipse, ByteString unknownFields) { + super(ADAPTER, unknownFields); + if (Internal.countNonNull(shape, rect, ellipse) > 1) { + throw new IllegalArgumentException("at most one of shape, rect, ellipse may be non-null"); + } + this.type = type; + this.styles = styles; + this.transform = transform; + this.shape = shape; + this.rect = rect; + this.ellipse = ellipse; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.type = type; + builder.styles = styles; + builder.transform = transform; + builder.shape = shape; + builder.rect = rect; + builder.ellipse = ellipse; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeEntity)) return false; + ShapeEntity o = (ShapeEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(type, o.type) + && Internal.equals(styles, o.styles) + && Internal.equals(transform, o.transform) + && Internal.equals(shape, o.shape) + && Internal.equals(rect, o.rect) + && Internal.equals(ellipse, o.ellipse); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (type != null ? type.hashCode() : 0); + result = result * 37 + (styles != null ? styles.hashCode() : 0); + result = result * 37 + (transform != null ? transform.hashCode() : 0); + result = result * 37 + (shape != null ? shape.hashCode() : 0); + result = result * 37 + (rect != null ? rect.hashCode() : 0); + result = result * 37 + (ellipse != null ? ellipse.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (type != null) builder.append(", type=").append(type); + if (styles != null) builder.append(", styles=").append(styles); + if (transform != null) builder.append(", transform=").append(transform); + if (shape != null) builder.append(", shape=").append(shape); + if (rect != null) builder.append(", rect=").append(rect); + if (ellipse != null) builder.append(", ellipse=").append(ellipse); + return builder.replace(0, 2, "ShapeEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public ShapeType type; + + public ShapeStyle styles; + + public com.opensource.svgaplayer.proto.Transform transform; + + public ShapeArgs shape; + + public RectArgs rect; + + public EllipseArgs ellipse; + + public Builder() { + } + + /** + * 矢量类型 + */ + public Builder type(ShapeType type) { + this.type = type; + return this; + } + + /** + * 矢量参数 + * 渲染参数 + */ + public Builder styles(ShapeStyle styles) { + this.styles = styles; + return this; + } + + /** + * 矢量图层 2D 变换矩阵 + */ + public Builder transform(com.opensource.svgaplayer.proto.Transform transform) { + this.transform = transform; + return this; + } + + public Builder shape(ShapeArgs shape) { + this.shape = shape; + this.rect = null; + this.ellipse = null; + return this; + } + + public Builder rect(RectArgs rect) { + this.rect = rect; + this.shape = null; + this.ellipse = null; + return this; + } + + public Builder ellipse(EllipseArgs ellipse) { + this.ellipse = ellipse; + this.shape = null; + this.rect = null; + return this; + } + + @Override + public ShapeEntity build() { + return new ShapeEntity(type, styles, transform, shape, rect, ellipse, super.buildUnknownFields()); + } + } + + public enum ShapeType implements WireEnum { + /** + * 路径 + */ + SHAPE(0), + + /** + * 矩形 + */ + RECT(1), + + /** + * 圆形 + */ + ELLIPSE(2), + + /** + * 与前帧一致 + */ + KEEP(3); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(ShapeType.class); + + private final int value; + + ShapeType(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static ShapeType fromValue(int value) { + switch (value) { + case 0: return SHAPE; + case 1: return RECT; + case 2: return ELLIPSE; + case 3: return KEEP; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + public static final class ShapeArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeArgs(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_D = ""; + + /** + * SVG 路径 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String d; + + public ShapeArgs(String d) { + this(d, ByteString.EMPTY); + } + + public ShapeArgs(String d, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.d = d; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.d = d; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeArgs)) return false; + ShapeArgs o = (ShapeArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(d, o.d); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (d != null ? d.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (d != null) builder.append(", d=").append(d); + return builder.replace(0, 2, "ShapeArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String d; + + public Builder() { + } + + /** + * SVG 路径 + */ + public Builder d(String d) { + this.d = d; + return this; + } + + @Override + public ShapeArgs build() { + return new ShapeArgs(d, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_ShapeArgs extends ProtoAdapter { + ProtoAdapter_ShapeArgs() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeArgs.class); + } + + @Override + public int encodedSize(ShapeArgs value) { + return (value.d != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.d) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeArgs value) throws IOException { + if (value.d != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.d); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.d(ProtoAdapter.STRING.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeArgs redact(ShapeArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class RectArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_RectArgs(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_WIDTH = 0.0f; + + public static final Float DEFAULT_HEIGHT = 0.0f; + + public static final Float DEFAULT_CORNERRADIUS = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float width; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float height; + + /** + * 圆角半径 + */ + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float cornerRadius; + + public RectArgs(Float x, Float y, Float width, Float height, Float cornerRadius) { + this(x, y, width, height, cornerRadius, ByteString.EMPTY); + } + + public RectArgs(Float x, Float y, Float width, Float height, Float cornerRadius, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.cornerRadius = cornerRadius; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.width = width; + builder.height = height; + builder.cornerRadius = cornerRadius; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof RectArgs)) return false; + RectArgs o = (RectArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(width, o.width) + && Internal.equals(height, o.height) + && Internal.equals(cornerRadius, o.cornerRadius); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (width != null ? width.hashCode() : 0); + result = result * 37 + (height != null ? height.hashCode() : 0); + result = result * 37 + (cornerRadius != null ? cornerRadius.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (width != null) builder.append(", width=").append(width); + if (height != null) builder.append(", height=").append(height); + if (cornerRadius != null) builder.append(", cornerRadius=").append(cornerRadius); + return builder.replace(0, 2, "RectArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float width; + + public Float height; + + public Float cornerRadius; + + public Builder() { + } + + public Builder x(Float x) { + this.x = x; + return this; + } + + public Builder y(Float y) { + this.y = y; + return this; + } + + public Builder width(Float width) { + this.width = width; + return this; + } + + public Builder height(Float height) { + this.height = height; + return this; + } + + /** + * 圆角半径 + */ + public Builder cornerRadius(Float cornerRadius) { + this.cornerRadius = cornerRadius; + return this; + } + + @Override + public RectArgs build() { + return new RectArgs(x, y, width, height, cornerRadius, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_RectArgs extends ProtoAdapter { + ProtoAdapter_RectArgs() { + super(FieldEncoding.LENGTH_DELIMITED, RectArgs.class); + } + + @Override + public int encodedSize(RectArgs value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.width != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.width) : 0) + + (value.height != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.height) : 0) + + (value.cornerRadius != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(5, value.cornerRadius) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, RectArgs value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.width != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.width); + if (value.height != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.height); + if (value.cornerRadius != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 5, value.cornerRadius); + writer.writeBytes(value.unknownFields()); + } + + @Override + public RectArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.width(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.height(ProtoAdapter.FLOAT.decode(reader)); break; + case 5: builder.cornerRadius(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public RectArgs redact(RectArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class EllipseArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_EllipseArgs(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_RADIUSX = 0.0f; + + public static final Float DEFAULT_RADIUSY = 0.0f; + + /** + * 圆中心点 X + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + /** + * 圆中心点 Y + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + /** + * 横向半径 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float radiusX; + + /** + * 纵向半径 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float radiusY; + + public EllipseArgs(Float x, Float y, Float radiusX, Float radiusY) { + this(x, y, radiusX, radiusY, ByteString.EMPTY); + } + + public EllipseArgs(Float x, Float y, Float radiusX, Float radiusY, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.radiusX = radiusX; + this.radiusY = radiusY; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.radiusX = radiusX; + builder.radiusY = radiusY; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof EllipseArgs)) return false; + EllipseArgs o = (EllipseArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(radiusX, o.radiusX) + && Internal.equals(radiusY, o.radiusY); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (radiusX != null ? radiusX.hashCode() : 0); + result = result * 37 + (radiusY != null ? radiusY.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (radiusX != null) builder.append(", radiusX=").append(radiusX); + if (radiusY != null) builder.append(", radiusY=").append(radiusY); + return builder.replace(0, 2, "EllipseArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float radiusX; + + public Float radiusY; + + public Builder() { + } + + /** + * 圆中心点 X + */ + public Builder x(Float x) { + this.x = x; + return this; + } + + /** + * 圆中心点 Y + */ + public Builder y(Float y) { + this.y = y; + return this; + } + + /** + * 横向半径 + */ + public Builder radiusX(Float radiusX) { + this.radiusX = radiusX; + return this; + } + + /** + * 纵向半径 + */ + public Builder radiusY(Float radiusY) { + this.radiusY = radiusY; + return this; + } + + @Override + public EllipseArgs build() { + return new EllipseArgs(x, y, radiusX, radiusY, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_EllipseArgs extends ProtoAdapter { + ProtoAdapter_EllipseArgs() { + super(FieldEncoding.LENGTH_DELIMITED, EllipseArgs.class); + } + + @Override + public int encodedSize(EllipseArgs value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.radiusX != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.radiusX) : 0) + + (value.radiusY != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.radiusY) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, EllipseArgs value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.radiusX != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.radiusX); + if (value.radiusY != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.radiusY); + writer.writeBytes(value.unknownFields()); + } + + @Override + public EllipseArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.radiusX(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.radiusY(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public EllipseArgs redact(EllipseArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class ShapeStyle extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeStyle(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_STROKEWIDTH = 0.0f; + + public static final LineCap DEFAULT_LINECAP = LineCap.LineCap_BUTT; + + public static final LineJoin DEFAULT_LINEJOIN = LineJoin.LineJoin_MITER; + + public static final Float DEFAULT_MITERLIMIT = 0.0f; + + public static final Float DEFAULT_LINEDASHI = 0.0f; + + public static final Float DEFAULT_LINEDASHII = 0.0f; + + public static final Float DEFAULT_LINEDASHIII = 0.0f; + + /** + * 填充色 + */ + @WireField( + tag = 1, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$RGBAColor#ADAPTER" + ) + public final RGBAColor fill; + + /** + * 描边色 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$RGBAColor#ADAPTER" + ) + public final RGBAColor stroke; + + /** + * 描边宽 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float strokeWidth; + + /** + * 线段端点样式 + */ + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$LineCap#ADAPTER" + ) + public final LineCap lineCap; + + /** + * 线段连接样式 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$LineJoin#ADAPTER" + ) + public final LineJoin lineJoin; + + /** + * 尖角限制 + */ + @WireField( + tag = 6, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float miterLimit; + + /** + * 虚线参数 Dash + */ + @WireField( + tag = 7, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashI; + + /** + * 虚线参数 Gap + */ + @WireField( + tag = 8, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashII; + + /** + * 虚线参数 Offset + */ + @WireField( + tag = 9, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashIII; + + public ShapeStyle(RGBAColor fill, RGBAColor stroke, Float strokeWidth, LineCap lineCap, LineJoin lineJoin, Float miterLimit, Float lineDashI, Float lineDashII, Float lineDashIII) { + this(fill, stroke, strokeWidth, lineCap, lineJoin, miterLimit, lineDashI, lineDashII, lineDashIII, ByteString.EMPTY); + } + + public ShapeStyle(RGBAColor fill, RGBAColor stroke, Float strokeWidth, LineCap lineCap, LineJoin lineJoin, Float miterLimit, Float lineDashI, Float lineDashII, Float lineDashIII, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.fill = fill; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + this.lineCap = lineCap; + this.lineJoin = lineJoin; + this.miterLimit = miterLimit; + this.lineDashI = lineDashI; + this.lineDashII = lineDashII; + this.lineDashIII = lineDashIII; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.fill = fill; + builder.stroke = stroke; + builder.strokeWidth = strokeWidth; + builder.lineCap = lineCap; + builder.lineJoin = lineJoin; + builder.miterLimit = miterLimit; + builder.lineDashI = lineDashI; + builder.lineDashII = lineDashII; + builder.lineDashIII = lineDashIII; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeStyle)) return false; + ShapeStyle o = (ShapeStyle) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(fill, o.fill) + && Internal.equals(stroke, o.stroke) + && Internal.equals(strokeWidth, o.strokeWidth) + && Internal.equals(lineCap, o.lineCap) + && Internal.equals(lineJoin, o.lineJoin) + && Internal.equals(miterLimit, o.miterLimit) + && Internal.equals(lineDashI, o.lineDashI) + && Internal.equals(lineDashII, o.lineDashII) + && Internal.equals(lineDashIII, o.lineDashIII); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (fill != null ? fill.hashCode() : 0); + result = result * 37 + (stroke != null ? stroke.hashCode() : 0); + result = result * 37 + (strokeWidth != null ? strokeWidth.hashCode() : 0); + result = result * 37 + (lineCap != null ? lineCap.hashCode() : 0); + result = result * 37 + (lineJoin != null ? lineJoin.hashCode() : 0); + result = result * 37 + (miterLimit != null ? miterLimit.hashCode() : 0); + result = result * 37 + (lineDashI != null ? lineDashI.hashCode() : 0); + result = result * 37 + (lineDashII != null ? lineDashII.hashCode() : 0); + result = result * 37 + (lineDashIII != null ? lineDashIII.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (fill != null) builder.append(", fill=").append(fill); + if (stroke != null) builder.append(", stroke=").append(stroke); + if (strokeWidth != null) builder.append(", strokeWidth=").append(strokeWidth); + if (lineCap != null) builder.append(", lineCap=").append(lineCap); + if (lineJoin != null) builder.append(", lineJoin=").append(lineJoin); + if (miterLimit != null) builder.append(", miterLimit=").append(miterLimit); + if (lineDashI != null) builder.append(", lineDashI=").append(lineDashI); + if (lineDashII != null) builder.append(", lineDashII=").append(lineDashII); + if (lineDashIII != null) builder.append(", lineDashIII=").append(lineDashIII); + return builder.replace(0, 2, "ShapeStyle{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public RGBAColor fill; + + public RGBAColor stroke; + + public Float strokeWidth; + + public LineCap lineCap; + + public LineJoin lineJoin; + + public Float miterLimit; + + public Float lineDashI; + + public Float lineDashII; + + public Float lineDashIII; + + public Builder() { + } + + /** + * 填充色 + */ + public Builder fill(RGBAColor fill) { + this.fill = fill; + return this; + } + + /** + * 描边色 + */ + public Builder stroke(RGBAColor stroke) { + this.stroke = stroke; + return this; + } + + /** + * 描边宽 + */ + public Builder strokeWidth(Float strokeWidth) { + this.strokeWidth = strokeWidth; + return this; + } + + /** + * 线段端点样式 + */ + public Builder lineCap(LineCap lineCap) { + this.lineCap = lineCap; + return this; + } + + /** + * 线段连接样式 + */ + public Builder lineJoin(LineJoin lineJoin) { + this.lineJoin = lineJoin; + return this; + } + + /** + * 尖角限制 + */ + public Builder miterLimit(Float miterLimit) { + this.miterLimit = miterLimit; + return this; + } + + /** + * 虚线参数 Dash + */ + public Builder lineDashI(Float lineDashI) { + this.lineDashI = lineDashI; + return this; + } + + /** + * 虚线参数 Gap + */ + public Builder lineDashII(Float lineDashII) { + this.lineDashII = lineDashII; + return this; + } + + /** + * 虚线参数 Offset + */ + public Builder lineDashIII(Float lineDashIII) { + this.lineDashIII = lineDashIII; + return this; + } + + @Override + public ShapeStyle build() { + return new ShapeStyle(fill, stroke, strokeWidth, lineCap, lineJoin, miterLimit, lineDashI, lineDashII, lineDashIII, super.buildUnknownFields()); + } + } + + public static final class RGBAColor extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_RGBAColor(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_R = 0.0f; + + public static final Float DEFAULT_G = 0.0f; + + public static final Float DEFAULT_B = 0.0f; + + public static final Float DEFAULT_A = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float r; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float g; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float b; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float a; + + public RGBAColor(Float r, Float g, Float b, Float a) { + this(r, g, b, a, ByteString.EMPTY); + } + + public RGBAColor(Float r, Float g, Float b, Float a, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.r = r; + builder.g = g; + builder.b = b; + builder.a = a; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof RGBAColor)) return false; + RGBAColor o = (RGBAColor) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(r, o.r) + && Internal.equals(g, o.g) + && Internal.equals(b, o.b) + && Internal.equals(a, o.a); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (r != null ? r.hashCode() : 0); + result = result * 37 + (g != null ? g.hashCode() : 0); + result = result * 37 + (b != null ? b.hashCode() : 0); + result = result * 37 + (a != null ? a.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (r != null) builder.append(", r=").append(r); + if (g != null) builder.append(", g=").append(g); + if (b != null) builder.append(", b=").append(b); + if (a != null) builder.append(", a=").append(a); + return builder.replace(0, 2, "RGBAColor{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float r; + + public Float g; + + public Float b; + + public Float a; + + public Builder() { + } + + public Builder r(Float r) { + this.r = r; + return this; + } + + public Builder g(Float g) { + this.g = g; + return this; + } + + public Builder b(Float b) { + this.b = b; + return this; + } + + public Builder a(Float a) { + this.a = a; + return this; + } + + @Override + public RGBAColor build() { + return new RGBAColor(r, g, b, a, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_RGBAColor extends ProtoAdapter { + ProtoAdapter_RGBAColor() { + super(FieldEncoding.LENGTH_DELIMITED, RGBAColor.class); + } + + @Override + public int encodedSize(RGBAColor value) { + return (value.r != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.r) : 0) + + (value.g != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.g) : 0) + + (value.b != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.b) : 0) + + (value.a != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.a) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, RGBAColor value) throws IOException { + if (value.r != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.r); + if (value.g != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.g); + if (value.b != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.b); + if (value.a != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.a); + writer.writeBytes(value.unknownFields()); + } + + @Override + public RGBAColor decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.r(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.g(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.b(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.a(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public RGBAColor redact(RGBAColor value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public enum LineCap implements WireEnum { + LineCap_BUTT(0), + + LineCap_ROUND(1), + + LineCap_SQUARE(2); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(LineCap.class); + + private final int value; + + LineCap(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static LineCap fromValue(int value) { + switch (value) { + case 0: return LineCap_BUTT; + case 1: return LineCap_ROUND; + case 2: return LineCap_SQUARE; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + public enum LineJoin implements WireEnum { + LineJoin_MITER(0), + + LineJoin_ROUND(1), + + LineJoin_BEVEL(2); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(LineJoin.class); + + private final int value; + + LineJoin(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static LineJoin fromValue(int value) { + switch (value) { + case 0: return LineJoin_MITER; + case 1: return LineJoin_ROUND; + case 2: return LineJoin_BEVEL; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + private static final class ProtoAdapter_ShapeStyle extends ProtoAdapter { + ProtoAdapter_ShapeStyle() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeStyle.class); + } + + @Override + public int encodedSize(ShapeStyle value) { + return (value.fill != null ? RGBAColor.ADAPTER.encodedSizeWithTag(1, value.fill) : 0) + + (value.stroke != null ? RGBAColor.ADAPTER.encodedSizeWithTag(2, value.stroke) : 0) + + (value.strokeWidth != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.strokeWidth) : 0) + + (value.lineCap != null ? LineCap.ADAPTER.encodedSizeWithTag(4, value.lineCap) : 0) + + (value.lineJoin != null ? LineJoin.ADAPTER.encodedSizeWithTag(5, value.lineJoin) : 0) + + (value.miterLimit != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(6, value.miterLimit) : 0) + + (value.lineDashI != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(7, value.lineDashI) : 0) + + (value.lineDashII != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(8, value.lineDashII) : 0) + + (value.lineDashIII != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(9, value.lineDashIII) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeStyle value) throws IOException { + if (value.fill != null) RGBAColor.ADAPTER.encodeWithTag(writer, 1, value.fill); + if (value.stroke != null) RGBAColor.ADAPTER.encodeWithTag(writer, 2, value.stroke); + if (value.strokeWidth != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.strokeWidth); + if (value.lineCap != null) LineCap.ADAPTER.encodeWithTag(writer, 4, value.lineCap); + if (value.lineJoin != null) LineJoin.ADAPTER.encodeWithTag(writer, 5, value.lineJoin); + if (value.miterLimit != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 6, value.miterLimit); + if (value.lineDashI != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 7, value.lineDashI); + if (value.lineDashII != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 8, value.lineDashII); + if (value.lineDashIII != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 9, value.lineDashIII); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeStyle decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.fill(RGBAColor.ADAPTER.decode(reader)); break; + case 2: builder.stroke(RGBAColor.ADAPTER.decode(reader)); break; + case 3: builder.strokeWidth(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: { + try { + builder.lineCap(LineCap.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 5: { + try { + builder.lineJoin(LineJoin.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 6: builder.miterLimit(ProtoAdapter.FLOAT.decode(reader)); break; + case 7: builder.lineDashI(ProtoAdapter.FLOAT.decode(reader)); break; + case 8: builder.lineDashII(ProtoAdapter.FLOAT.decode(reader)); break; + case 9: builder.lineDashIII(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeStyle redact(ShapeStyle value) { + Builder builder = value.newBuilder(); + if (builder.fill != null) builder.fill = RGBAColor.ADAPTER.redact(builder.fill); + if (builder.stroke != null) builder.stroke = RGBAColor.ADAPTER.redact(builder.stroke); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + private static final class ProtoAdapter_ShapeEntity extends ProtoAdapter { + ProtoAdapter_ShapeEntity() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeEntity.class); + } + + @Override + public int encodedSize(ShapeEntity value) { + return (value.type != null ? ShapeType.ADAPTER.encodedSizeWithTag(1, value.type) : 0) + + (value.styles != null ? ShapeStyle.ADAPTER.encodedSizeWithTag(10, value.styles) : 0) + + (value.transform != null ? com.opensource.svgaplayer.proto.Transform.ADAPTER.encodedSizeWithTag(11, value.transform) : 0) + + (value.shape != null ? ShapeArgs.ADAPTER.encodedSizeWithTag(2, value.shape) : 0) + + (value.rect != null ? RectArgs.ADAPTER.encodedSizeWithTag(3, value.rect) : 0) + + (value.ellipse != null ? EllipseArgs.ADAPTER.encodedSizeWithTag(4, value.ellipse) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeEntity value) throws IOException { + if (value.type != null) ShapeType.ADAPTER.encodeWithTag(writer, 1, value.type); + if (value.styles != null) ShapeStyle.ADAPTER.encodeWithTag(writer, 10, value.styles); + if (value.transform != null) com.opensource.svgaplayer.proto.Transform.ADAPTER.encodeWithTag(writer, 11, value.transform); + if (value.shape != null) ShapeArgs.ADAPTER.encodeWithTag(writer, 2, value.shape); + if (value.rect != null) RectArgs.ADAPTER.encodeWithTag(writer, 3, value.rect); + if (value.ellipse != null) EllipseArgs.ADAPTER.encodeWithTag(writer, 4, value.ellipse); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: { + try { + builder.type(ShapeType.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 10: builder.styles(ShapeStyle.ADAPTER.decode(reader)); break; + case 11: builder.transform(com.opensource.svgaplayer.proto.Transform.ADAPTER.decode(reader)); break; + case 2: builder.shape(ShapeArgs.ADAPTER.decode(reader)); break; + case 3: builder.rect(RectArgs.ADAPTER.decode(reader)); break; + case 4: builder.ellipse(EllipseArgs.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeEntity redact(ShapeEntity value) { + Builder builder = value.newBuilder(); + if (builder.styles != null) builder.styles = ShapeStyle.ADAPTER.redact(builder.styles); + if (builder.transform != null) builder.transform = Transform.ADAPTER.redact(builder.transform); + if (builder.shape != null) builder.shape = ShapeArgs.ADAPTER.redact(builder.shape); + if (builder.rect != null) builder.rect = RectArgs.ADAPTER.redact(builder.rect); + if (builder.ellipse != null) builder.ellipse = EllipseArgs.ADAPTER.redact(builder.ellipse); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java b/library/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java new file mode 100644 index 0000000..18e75a9 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java @@ -0,0 +1,200 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 13:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; +import java.util.List; + +import okio.ByteString; + +public final class SpriteEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_SpriteEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_IMAGEKEY = ""; + + public static final String DEFAULT_MATTEKEY = ""; + + /** + * 元件所对应的位图键名, 如果 imageKey 含有 .vector 后缀,该 sprite 为矢量图层 含有 .matte 后缀,该 sprite 为遮罩图层。 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String imageKey; + + /** + * 帧列表 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.FrameEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List frames; + + /** + * 被遮罩图层的 matteKey 对应的是其遮罩图层的 imageKey. + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String matteKey; + + public SpriteEntity(String imageKey, List frames, String matteKey) { + this(imageKey, frames, matteKey, ByteString.EMPTY); + } + + public SpriteEntity(String imageKey, List frames, String matteKey, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.imageKey = imageKey; + this.frames = Internal.immutableCopyOf("frames", frames); + this.matteKey = matteKey; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.imageKey = imageKey; + builder.frames = Internal.copyOf("frames", frames); + builder.matteKey = matteKey; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof SpriteEntity)) return false; + SpriteEntity o = (SpriteEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(imageKey, o.imageKey) + && frames.equals(o.frames) + && Internal.equals(matteKey, o.matteKey); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (imageKey != null ? imageKey.hashCode() : 0); + result = result * 37 + frames.hashCode(); + result = result * 37 + (matteKey != null ? matteKey.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (imageKey != null) builder.append(", imageKey=").append(imageKey); + if (!frames.isEmpty()) builder.append(", frames=").append(frames); + if (matteKey != null) builder.append(", matteKey=").append(matteKey); + return builder.replace(0, 2, "SpriteEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String imageKey; + + public List frames; + + public String matteKey; + + public Builder() { + frames = Internal.newMutableList(); + } + + /** + * 元件所对应的位图键名, 如果 imageKey 含有 .vector 后缀,该 sprite 为矢量图层 含有 .matte 后缀,该 sprite 为遮罩图层。 + */ + public Builder imageKey(String imageKey) { + this.imageKey = imageKey; + return this; + } + + /** + * 帧列表 + */ + public Builder frames(List frames) { + Internal.checkElementsNotNull(frames); + this.frames = frames; + return this; + } + + /** + * 被遮罩图层的 matteKey 对应的是其遮罩图层的 imageKey. + */ + public Builder matteKey(String matteKey) { + this.matteKey = matteKey; + return this; + } + + @Override + public SpriteEntity build() { + return new SpriteEntity(imageKey, frames, matteKey, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_SpriteEntity extends ProtoAdapter { + ProtoAdapter_SpriteEntity() { + super(FieldEncoding.LENGTH_DELIMITED, SpriteEntity.class); + } + + @Override + public int encodedSize(SpriteEntity value) { + return (value.imageKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.imageKey) : 0) + + com.opensource.svgaplayer.proto.FrameEntity.ADAPTER.asRepeated().encodedSizeWithTag(2, value.frames) + + (value.matteKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(3, value.matteKey) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, SpriteEntity value) throws IOException { + if (value.imageKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.imageKey); + com.opensource.svgaplayer.proto.FrameEntity.ADAPTER.asRepeated().encodeWithTag(writer, 2, value.frames); + if (value.matteKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 3, value.matteKey); + writer.writeBytes(value.unknownFields()); + } + + @Override + public SpriteEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.imageKey(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.frames.add(com.opensource.svgaplayer.proto.FrameEntity.ADAPTER.decode(reader)); break; + case 3: builder.matteKey(ProtoAdapter.STRING.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public SpriteEntity redact(SpriteEntity value) { + Builder builder = value.newBuilder(); + Internal.redactElements(builder.frames, FrameEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/proto/Transform.java b/library/src/main/java/com/opensource/svgaplayer/proto/Transform.java new file mode 100644 index 0000000..0018734 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/proto/Transform.java @@ -0,0 +1,248 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 34:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; + +import java.io.IOException; + +import okio.ByteString; + +public final class Transform extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_Transform(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_A = 0.0f; + + public static final Float DEFAULT_B = 0.0f; + + public static final Float DEFAULT_C = 0.0f; + + public static final Float DEFAULT_D = 0.0f; + + public static final Float DEFAULT_TX = 0.0f; + + public static final Float DEFAULT_TY = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float a; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float b; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float c; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float d; + + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float tx; + + @WireField( + tag = 6, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float ty; + + public Transform(Float a, Float b, Float c, Float d, Float tx, Float ty) { + this(a, b, c, d, tx, ty, ByteString.EMPTY); + } + + public Transform(Float a, Float b, Float c, Float d, Float tx, Float ty, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.tx = tx; + this.ty = ty; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.a = a; + builder.b = b; + builder.c = c; + builder.d = d; + builder.tx = tx; + builder.ty = ty; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof Transform)) return false; + Transform o = (Transform) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(a, o.a) + && Internal.equals(b, o.b) + && Internal.equals(c, o.c) + && Internal.equals(d, o.d) + && Internal.equals(tx, o.tx) + && Internal.equals(ty, o.ty); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (a != null ? a.hashCode() : 0); + result = result * 37 + (b != null ? b.hashCode() : 0); + result = result * 37 + (c != null ? c.hashCode() : 0); + result = result * 37 + (d != null ? d.hashCode() : 0); + result = result * 37 + (tx != null ? tx.hashCode() : 0); + result = result * 37 + (ty != null ? ty.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (a != null) builder.append(", a=").append(a); + if (b != null) builder.append(", b=").append(b); + if (c != null) builder.append(", c=").append(c); + if (d != null) builder.append(", d=").append(d); + if (tx != null) builder.append(", tx=").append(tx); + if (ty != null) builder.append(", ty=").append(ty); + return builder.replace(0, 2, "Transform{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float a; + + public Float b; + + public Float c; + + public Float d; + + public Float tx; + + public Float ty; + + public Builder() { + } + + public Builder a(Float a) { + this.a = a; + return this; + } + + public Builder b(Float b) { + this.b = b; + return this; + } + + public Builder c(Float c) { + this.c = c; + return this; + } + + public Builder d(Float d) { + this.d = d; + return this; + } + + public Builder tx(Float tx) { + this.tx = tx; + return this; + } + + public Builder ty(Float ty) { + this.ty = ty; + return this; + } + + @Override + public Transform build() { + return new Transform(a, b, c, d, tx, ty, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_Transform extends ProtoAdapter { + ProtoAdapter_Transform() { + super(FieldEncoding.LENGTH_DELIMITED, Transform.class); + } + + @Override + public int encodedSize(Transform value) { + return (value.a != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.a) : 0) + + (value.b != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.b) : 0) + + (value.c != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.c) : 0) + + (value.d != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.d) : 0) + + (value.tx != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(5, value.tx) : 0) + + (value.ty != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(6, value.ty) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, Transform value) throws IOException { + if (value.a != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.a); + if (value.b != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.b); + if (value.c != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.c); + if (value.d != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.d); + if (value.tx != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 5, value.tx); + if (value.ty != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 6, value.ty); + writer.writeBytes(value.unknownFields()); + } + + @Override + public Transform decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.a(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.b(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.c(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.d(ProtoAdapter.FLOAT.decode(reader)); break; + case 5: builder.tx(ProtoAdapter.FLOAT.decode(reader)); break; + case 6: builder.ty(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public Transform redact(Transform value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/Pools.kt b/library/src/main/java/com/opensource/svgaplayer/utils/Pools.kt new file mode 100644 index 0000000..7382ab8 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/Pools.kt @@ -0,0 +1,102 @@ +package com.opensource.svgaplayer.utils + +/** + * Helper class for creating pools of objects. An example use looks like this: + *
+ * public class MyPooledClass {
+ *
+ *     private static final SynchronizedPool sPool =
+ *             new SynchronizedPool(10);
+ *
+ *     public static MyPooledClass obtain() {
+ *         MyPooledClass instance = sPool.acquire();
+ *         return (instance != null) ? instance : new MyPooledClass();
+ *     }
+ *
+ *     public void recycle() {
+ *          // Clear state if needed.
+ *          sPool.release(this);
+ *     }
+ *
+ *     . . .
+ * }
+ * 
+ * + */ +class Pools private constructor() { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + interface Pool { + /** + * @return An instance from the pool if such, null otherwise. + */ + fun acquire(): T? + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + fun release(instance: T): Boolean + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + * + * @param The pooled type. + */ + open class SimplePool(maxPoolSize: Int) : Pool { + private val mPool: Array + private var mPoolSize = 0 + + init { + require(maxPoolSize > 0) { "The max pool size must be > 0" } + mPool = arrayOfNulls(maxPoolSize) + } + + @Suppress("UNCHECKED_CAST") + override fun acquire(): T? { + if (mPoolSize > 0) { + val lastPooledIndex = mPoolSize - 1 + val instance = mPool[lastPooledIndex] as T? + mPool[lastPooledIndex] = null + mPoolSize-- + return instance + } + return null + } + + override fun release(instance: T): Boolean { + check(!isInPool(instance)) { "Already in the pool!" } + if (mPoolSize < mPool.size) { + mPool[mPoolSize] = instance + mPoolSize++ + return true + } + return false + } + + private fun isInPool(instance: T): Boolean { + for (i in 0 until mPoolSize) { + if (mPool[i] === instance) { + return true + } + } + return false + } + + } + + +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt b/library/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt new file mode 100644 index 0000000..792abc2 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt @@ -0,0 +1,146 @@ +package com.opensource.svgaplayer.utils + +import android.widget.ImageView + +/** + * Created by ubt on 2018/1/19. + */ +class SVGAScaleInfo { + + var tranFx : Float = 0.0f + var tranFy : Float = 0.0f + var scaleFx : Float = 1.0f + var scaleFy : Float = 1.0f + var ratio = 1.0f + var ratioX = false + + private fun resetVar(){ + tranFx = 0.0f + tranFy = 0.0f + scaleFx = 1.0f + scaleFy = 1.0f + ratio = 1.0f + ratioX = false + } + + fun performScaleType(canvasWidth : Float, canvasHeight: Float, videoWidth : Float, videoHeight : Float, scaleType: ImageView.ScaleType) { + if (canvasWidth == 0.0f || canvasHeight == 0.0f || videoWidth == 0.0f || videoHeight == 0.0f) { + return + } + + resetVar() + val canW_vidW_f = (canvasWidth - videoWidth) / 2.0f + val canH_vidH_f = (canvasHeight - videoHeight) / 2.0f + + val videoRatio = videoWidth / videoHeight + val canvasRatio = canvasWidth / canvasHeight + + val canH_d_vidH = canvasHeight / videoHeight + val canW_d_vidW = canvasWidth / videoWidth + + when (scaleType) { + ImageView.ScaleType.CENTER -> { + tranFx = canW_vidW_f + tranFy = canH_vidH_f + } + ImageView.ScaleType.CENTER_CROP -> { + if (videoRatio > canvasRatio) { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + else { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + } + } + ImageView.ScaleType.CENTER_INSIDE -> { + if (videoWidth < canvasWidth && videoHeight < canvasHeight) { + tranFx = canW_vidW_f + tranFy = canH_vidH_f + } + else { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + } + } + ImageView.ScaleType.FIT_CENTER -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + } + ImageView.ScaleType.FIT_START -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + } + } + ImageView.ScaleType.FIT_END -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy= canvasHeight - videoHeight * (canW_d_vidW) + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = canvasWidth - videoWidth * (canH_d_vidH) + } + } + ImageView.ScaleType.FIT_XY -> { + ratio = Math.max(canW_d_vidW, canH_d_vidH) + ratioX = canW_d_vidW > canH_d_vidH + scaleFx = canW_d_vidW + scaleFy = canH_d_vidH + } + else -> { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + } + } + } + +} diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt b/library/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt new file mode 100644 index 0000000..f87b30d --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt @@ -0,0 +1,11 @@ +package com.opensource.svgaplayer.utils + +/** + * Created by cuiminghui on 2017/3/29. + */ + +class SVGAPoint(val x: Float, val y: Float, val value: Float) + +class SVGARect(val x: Double, val y: Double, val width: Double, val height: Double) + +class SVGARange(val location: Int, val length: Int) \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt b/library/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt new file mode 100644 index 0000000..33200b0 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt @@ -0,0 +1,28 @@ +package com.opensource.svgaplayer.utils.log + +import android.util.Log + +/** + * 内部默认 ILogger 接口实现 + */ +class DefaultLogCat : ILogger { + override fun verbose(tag: String, msg: String) { + Log.v(tag, msg) + } + + override fun info(tag: String, msg: String) { + Log.i(tag, msg) + } + + override fun debug(tag: String, msg: String) { + Log.d(tag, msg) + } + + override fun warn(tag: String, msg: String) { + Log.w(tag, msg) + } + + override fun error(tag: String, msg: String?, error: Throwable?) { + Log.e(tag, msg, error) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt b/library/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt new file mode 100644 index 0000000..ad93510 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt @@ -0,0 +1,12 @@ +package com.opensource.svgaplayer.utils.log + +/** + * log 外部接管接口 + **/ +interface ILogger { + fun verbose(tag: String, msg: String) + fun info(tag: String, msg: String) + fun debug(tag: String, msg: String) + fun warn(tag: String, msg: String) + fun error(tag: String, msg: String?, error: Throwable?) +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt b/library/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt new file mode 100644 index 0000000..60c67f9 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt @@ -0,0 +1,57 @@ +package com.opensource.svgaplayer.utils.log + +/** + * 日志输出 + */ +internal object LogUtils { + private const val TAG = "SVGALog" + + fun verbose(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.verbose(tag, msg) + } + + fun info(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.info(tag, msg) + } + + fun debug(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.debug(tag, msg) + } + + fun warn(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.warn(tag, msg) + } + + fun error(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, msg, null) + } + + fun error(tag: String, error: Throwable) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, error.message, error) + } + + fun error(tag: String = TAG, msg: String, error: Throwable) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, msg, error) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt b/library/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt new file mode 100644 index 0000000..5767c63 --- /dev/null +++ b/library/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt @@ -0,0 +1,40 @@ +package com.opensource.svgaplayer.utils.log + +/** + * SVGA logger 配置管理 + **/ +object SVGALogger { + + private var mLogger: ILogger? = DefaultLogCat() + private var isLogEnabled = false + + /** + * log 接管注入 + */ + fun injectSVGALoggerImp(logImp: ILogger): SVGALogger { + mLogger = logImp + return this + } + + /** + * 设置是否开启 log + */ + fun setLogEnabled(isEnabled: Boolean): SVGALogger { + isLogEnabled = isEnabled + return this + } + + /** + * 获取当前 ILogger 实现类 + */ + fun getSVGALogger(): ILogger? { + return mLogger + } + + /** + * 是否开启 log + */ + fun isLogEnabled(): Boolean { + return isLogEnabled + } +} \ No newline at end of file diff --git a/library/src/main/res/drawable-v24/ic_launcher_foreground.xml b/library/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..300243f --- /dev/null +++ b/library/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/library/src/main/res/drawable-xhdpi/ic_decoration.webp b/library/src/main/res/drawable-xhdpi/ic_decoration.webp new file mode 100644 index 0000000000000000000000000000000000000000..d0994bb2ae7902bd179fb1f24a521330b0de06c9 GIT binary patch literal 1590 zcmV-62FdwSNk&Er1^@t8MM6+kP&il$0000G0001Y004IY06|PpNdE!=00AfCAd)0S zx*U8)|7t(pLqz03V$n)+O&uhb2{tGg<4Jy@APH^^<^KWa@T&|8>DsoZZKF_`nISMJ zGcz-DYp2b0{wLAYv8C_3_Sdhzi2hH2GQXzp&)nM4?em+bV|l~v(VF|ySEFc8_e>u= z>5$>3d(uSJkL`LoVt;?kky9NXmb8&_G+s?f=W0Q5Z z+Kn0XHL(IiM!z(0|J@!}KDfdwS8~JE%^_EAgDaWiYW^=*V|`q?Ip`w_KO~uQ)UkN z)QE$r9j~+ql(~%;lw1f*u3DsHsY)4M_@Kf4R_3>!P;e^B#^jV|n@l-a+O%WWZWAmX zxU$C$!0tr$!4AnKl=aE%sd)}bMPt|Mr%6~9MQHW-amfrhhEvqmkL`L<`TJu|(VXs? zK6nZXZn`I}Xs_)bn_EA=dwDDB`18%(9k0)gNv$#;09H^qAmjo70FV;^odGI%0C)gC zkw~9QrKF;vqZL@tuo4MP+o(`KplM9?@5m3uUY%DCwSHvWk?Qs}Lvg*HomsW^Nxof{xo<^~iAgu%ef^FaQAl zu5b1M05~)Kpug?Jhk~L0t^fg1crz9!Pj64(Kv?%e_aNL`N_ylC{fnc23RS4U&}NkcwNSN+SWfdr}Hl;miQzKtU;FakgFyi`ss!c;$EnMu{(!XmsbvV$~GpP1kkrO-hR{+tOBsD=uw zg=i7`GAfKhGnuXGT_2eym+=fsOnjZ4xt^io4gM;bMk?BH_9$E*#Na`dE2ln}hiDKK zW66_I7fBpHUq$f(rn<0l-PGAorB1EIkA)GAew~Be1!t;RhU{8^^F8#e2=^{4^_C*u zhwrgD{2D0YTJ8i$Nj)y3zK&4V!z`IS=Y_t$4~tNmK7e}susI=f&wx{Xg*qvPtqHG2 zPYMa_r|>SOSBGqYH93@F?Sv`(^uwR(wVnN0QmQcS`_~$IcGxcWEYE1LTDLqeTGwHN zCDB?n#Y`j`Y}#zIreB;#N#_R@RdSYz&%=mr_2$+GkY3=0U!BI&Zf&##$;F-J2;jUQ z+F<6|2{1%(F2LNs2a|VJ{{a6O;oL7q?N)PhXoQIS>UzMr;s$VXl4feF+)csjfB*mh o05Lc@Ff(ChVPrBmWHn_pI5IXlVliSeGc{o}F*Pz}F(4o<3a>5wd;kCd literal 0 HcmV?d00001 diff --git a/library/src/main/res/drawable-xhdpi/shadow_bottom.webp b/library/src/main/res/drawable-xhdpi/shadow_bottom.webp new file mode 100644 index 0000000000000000000000000000000000000000..c76740036e4d80ed0b0944c9959201096084c2ef GIT binary patch literal 212 zcmWIYbaPw5z`zjh>J$(bU=hIuWD5YXJp+RR1B0VafJXq3$H<zqij~Q~EW({@-Ky`k#mM>po7FuX!#V|MO6~-N%Xid!EbYS02ue z`#4G5=DDPQ<)LJ`j}zEsfHo-@0nII7W|+&U<-nlGU^mfjrpMC7N(*&87&ZuTW->6O z9!l1YJp2Cw!@nQrD_4lh|ExU#)M030X<(d~mY8B>kz$%^Vqs)vnPiw`WNeyfVrXiV KYN()~%LM>CrALMU literal 0 HcmV?d00001 diff --git a/library/src/main/res/drawable-xhdpi/shadow_left.webp b/library/src/main/res/drawable-xhdpi/shadow_left.webp new file mode 100644 index 0000000000000000000000000000000000000000..bda82862602c9ec065c5d6cdfe5a577eeede8de4 GIT binary patch literal 176 zcmWIYbaQKAU|b%7 literal 0 HcmV?d00001 diff --git a/library/src/main/res/drawable-xhdpi/shadow_right.webp b/library/src/main/res/drawable-xhdpi/shadow_right.webp new file mode 100644 index 0000000000000000000000000000000000000000..03f9685c4960c14e70fae8d0d0e5200a44bb1f7b GIT binary patch literal 174 zcmWIYbaQKBU|@CdsruhiSok)V-v zo9)t!J1n;hHR?kaZ*X_{ub#jq@V)DV(SI=ppcx8AKph3l409Q^6d4>C>?YdH^jNxB zX`!wM!v-PFOa_M3Fs()Q|6g2S`1j*{ZT`Rbrx$=47z`~e4U7}h5>t#UQcP1#ER4)7 VlMIuLj7<|w3{8zv4HXo0xd3@)GeH0V literal 0 HcmV?d00001 diff --git a/library/src/main/res/drawable-xhdpi/user_ucrop_ic_closs.webp b/library/src/main/res/drawable-xhdpi/user_ucrop_ic_closs.webp new file mode 100644 index 0000000000000000000000000000000000000000..cbd0d8e748e1590b2dd16b689efcd96a87da3f58 GIT binary patch literal 497 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`xTHpSruq6Z zXaU(A42G$TlC0TWzSWdSpS4N|DZ_xKM`nYpKnV@L$& z+v$mX%?doNXZyA`ztDTCw@3cn-XnTTG^aLAm~pekfV1{*&4R<3yB?Q4h?lvsE!Qpj z;=J^`##_tIS}y5SIpk6E;jyTYfm$$gZECG;fnDc!G(LdCUue~Fe#MFgu^PGe2Yg*|!Hfjvij%mC)wsny}0H>VcBxtVT;&E`y!QIX^lZ z!`{8rwm;-_#Og_VqR{d+<>4|k1|%Oc%$NbB7>k44ofy`glX(f`xTHpSruq6Z zXaU(A42G$TlC0TWzSWdSpS4N|DZ_xKM`nY^cqV@L$& z+iCV(hYUp8SHFGpNTH%rZ{C5E7n5I9mR#cIIM)|~MFGt@67FK;-u>ha=M zh2`f&eS52fli$>77@R0^i>X@JTFtbUiFI~D*sY-NcR7Nee^fX(A*GiuqxsUVq}=m2 zZ~vdI*m>YZVbX-HqUO(UUY2*8H0i~P%TxaP1vDR=;&7Wwp)!TlRKtgJ%A_wkETR`V zj{ZvQ^K|&4P}pYWXxSW?5W8T`;gZHrY{{IZj4R$PDk(SLDkFb6UUB<2YYFe=A`|5$ zyfyY4=msPv@T{6=dOGvR}w8Nf%-R!kDBmO*>`N5#7+G6`^!vcR$ka@cLxvX + + + + \ No newline at end of file diff --git a/library/src/main/res/layout/layout_user_card_item.xml b/library/src/main/res/layout/layout_user_card_item.xml new file mode 100644 index 0000000..edd1b82 --- /dev/null +++ b/library/src/main/res/layout/layout_user_card_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/src/main/res/layout/swipeback_layout.xml b/library/src/main/res/layout/swipeback_layout.xml new file mode 100644 index 0000000..79935c5 --- /dev/null +++ b/library/src/main/res/layout/swipeback_layout.xml @@ -0,0 +1,7 @@ + + + diff --git a/library/src/main/res/values-ar/strings.xml b/library/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..11409af --- /dev/null +++ b/library/src/main/res/values-ar/strings.xml @@ -0,0 +1,130 @@ +]> + + + &app_name; + + هذه اللحظة + اليوم + أمس + قبل يومين + %1$d شهر %2$d يوم + %1$d سنة %2$d شهر %3$d يوم + %1$d دقائق مضت + %1$d ساعة مضت + يمكن استدعاء هذه الطريقة فقط قبل الحصول على MvpPresenter() ، إذا تم بالفعل إنشاء المقدم ، فلا يمكن تغييره!!! + فشل إنشاء المقدم ، تحقق مما إذا تم تعريف الإعلان @CreatePresenter(xxx.class)!!!---- + لا يوجد شبكة ، قوة الوصول إلى التخزين المؤقت! + + يرجى استدعاء طريقة RxNet.init() للتهيئة في المشروع!!! + جارٍ تنفيذ المهمة الخلفية...%d + تنفيذ حذف ملفات السجلات + نجاح + فشل + جارٍ إيقاف المهمة الخلفية...%d + استراتيجية التخلي عن التنفيذ عالية الوحدة المتعددة لحوض السباحة الفردي + استراتيجية التخلي عن التنفيذ لزيادة التوازي الشديد + :وقت تنفيذ BufferedOutputStream: + مللي ثانية + التشفير DES للنص بعد base64: + نص غير معالج: + البيانات المفككة: + جارٍ التنزيل + تم إلغاء التنزيل + التجوال إلى الحد الأقصى لعدد الطبقات + الدليل يحتوي على ملفات غير قياسية + الدليل فارغ + تصفية للحصول على معلومات الجدول mout: + تصفية معلومات mout بعد الدليل الفرعي: + تصفية معلومات mout بعد نفس الدليل: + [جهاز نقطة التثبيت:%s;المسار:%s;الحجم:%s;النوع:%s] + عشرة آلاف + مليار + تريليون + استثناء في التنسيق. + عشرة آلاف + + الأصلي: + زاوية دوران الأصلي: + العرض والارتفاع، + الجودة قبل الضغط: + معلمات الضغط: + زاوية دوران الصورة المضغوطة: + فشل الضغط... + خطأ في الضغط على الصورة + خطأ في تحليل parseLong + خطأ في تحليل parseInt + خطأ في تحويل str2double + خطأ في تحويل str2double2len + خطأ في تحويل str2double0len + خطأ في تحليل parseFloat + واحد + اثنان + ثلاثة + أربعة + خمسة + ستة + سبعة + يوم + أقل من 6.0 + أقل من 6.0 + أقل من 6.0 + أعلى من 6.0 وأقل من 7.0 + أعلى من 6.0 وأقل من 7.0 + أعلى من 6.0 وأقل من 7.0 + أعلى من 7.0 + أعلى من 7.0 1 + 7.0 وأعلى 1 + 7.0 وأعلى 1 + 7.0 وأعلى 2 + 7.0 وأعلى 2 + 7.0 وأعلى 2 + 7.0 وأعلى 3 + 7.0 وأعلى 3 + 7.0 وأعلى 3 + استثناء في الشبكة + لا يمكن أن يكون فارغًا! + فشل في حفظ الكائن + + الهاتف المحمول الصيني + الهاتف الصيني للاتصالات + الهاتف الصيني للاتصالات البعيدة + غير معروف + الجدي + الدلو + الدلو + الحوت + الحوت + الحمل + الحمل + الثور + الثور + الجوزاء + الجوزاء + السرطان + السرطان + الأسد + الأسد + العذراء + العذراء + الميزان + الميزان + العقرب + العقرب + القوس + القوس + الجدي + أثناء ساعات العمل + بعد ساعات العمل + أثناء ساعات العمل + بعد ساعات العمل + إصدار بيتا + تم بناء المسار الكامل للملف المضغوط بنجاح: %s + تعيين تباعد أسفل + و + فشل الطلب، يرجى المحاولة مرة أخرى في وقت لاحق + استثناء في تحليل البيانات + شبكة غير طبيعية، يرجى التحقق من الشبكة والمحاولة مرة أخرى + خطأ في المعلمة + + + diff --git a/library/src/main/res/values-zh-rTW/strings.xml b/library/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..95044f1 --- /dev/null +++ b/library/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,127 @@ +]> + + &app_name; + + 剛剛 + 今天 + 昨天 + 前天 + %1$d月%2$d日 + %1$d年%2$d月%3$d日 + %1$d分鐘前 + %1$d小時前 + 這個方法只能在getMvpPresenter()之前調用,如果Presenter已經創建了則不能再更改!!! + Presenter 創建失敗,檢查是否聲明了@CreatePresenter(xxx.class)註解!!!---- + 沒有網絡,強製獲取緩存! + 請在項目中先調用RxNet.init()方法初始化!!! + 開始後臺任務......%d + 執行刪除日誌文件 + 成功 + 失敗 + 停止後臺任務......%d + 單線程池並發過高執行拋棄策略 + 光暈並發過高執行拋棄策略 + :BufferedOutputStream執行耗時: + 豪秒 + text DES加密後base64: + 未處理原文: + 解密後數據: + 正在下載 + 已經取消下載 + 遍歷到限製最大層數了 + 目錄包含非標準文件 + 目錄為空 + 過濾得到的mout表信息: + 過濾子目錄後得到的mout信息: + 過濾相同目錄後得到的mout信息: + [掛載點 device:%s;path:%s;size:%s;type:%s] + + + + 格式化異常。 + + 原圖: + 原圖旋轉角度: + 寬高, + 壓縮前質量: + 壓縮參數: + 壓縮圖旋轉角度: + 壓縮失敗... + 壓縮圖片OOM了 + parseLong錯誤 + parseInt錯誤 + str2double錯誤 + str2double2len錯誤 + str2double0len錯誤 + parseFloat錯誤 + + + + + + + + + 6.0以下 + 6.0以下 + 6.0以下 + 6.0以上7.0以下 + 6.0以上7.0以下 + 6.0以上7.0以下 + 7.0以上 + 7.0以上1 + 7.0以上1 + 7.0以上1 + 7.0以上2 + 7.0以上2 + 7.0以上2 + 7.0以上3 + 7.0以上3 + 7.0以上3 + 網絡異常 + 不能為null ! + 保存obj失敗 + 中國移動 + 中國電信 + 中國聯通 + 未知 + 摩羯座 + 水瓶座 + 水瓶座 + 雙魚座 + 雙魚座 + 白羊座 + 白羊座 + 金牛座 + 金牛座 + 雙子座 + 雙子座 + 巨蟹座 + 巨蟹座 + 獅子座 + 獅子座 + 處女座 + 處女座 + 天秤座 + 天秤座 + 天蠍座 + 天蠍座 + 射手座 + 射手座 + 摩羯座 + 工作時間內 + 工作時間外 + 工作時間內 + 工作時間外 + 內測版 + 構建zip完整路徑成功: %s + 設置底部間距 + w + + 請求失敗,請稍後重試 + 數據解析異常 + 網絡異常,請檢查您的網絡再試~ + 參數錯誤 + + + diff --git a/library/src/main/res/values/arrays.xml b/library/src/main/res/values/arrays.xml new file mode 100644 index 0000000..c610502 --- /dev/null +++ b/library/src/main/res/values/arrays.xml @@ -0,0 +1,17 @@ + + + + 一月 + 二月 + 三月 + 四月 + 五月 + 六月 + 七月 + 八月 + 九月 + 十月 + 十一月 + 十二月 + + \ No newline at end of file diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml new file mode 100644 index 0000000..02524d2 --- /dev/null +++ b/library/src/main/res/values/attrs.xml @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/values/colors.xml b/library/src/main/res/values/colors.xml new file mode 100644 index 0000000..1dba054 --- /dev/null +++ b/library/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FFFFFF + #B3B3C3 + diff --git a/library/src/main/res/values/dimens.xml b/library/src/main/res/values/dimens.xml new file mode 100644 index 0000000..de27c83 --- /dev/null +++ b/library/src/main/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + 5dp + 4dp + 3dp + + 50dp + + 40.0dp + 50.0dp + 25.0dp + 100.0dp + 150.0dp + 30.0dp + + 27.3dp + 32.5dp + + 16sp + \ No newline at end of file diff --git a/library/src/main/res/values/integers.xml b/library/src/main/res/values/integers.xml new file mode 100644 index 0000000..499c035 --- /dev/null +++ b/library/src/main/res/values/integers.xml @@ -0,0 +1,7 @@ + + + + 6 + 3000 + + \ No newline at end of file diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 0000000..b357233 --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,126 @@ +]> + + &app_name; + + Just now + Today + Yesterday + Day before yesterday + %1$d Month %2$d Day + %1$d Year %2$d Month %3$d Day + %1$d Minutes ago + %1$d Hours ago + This method can only be called before getMvpPresenter(), if the Presenter has already been created, it cannot be changed!!! + Presenter creation failed, check if @CreatePresenter(xxx.class) annotation is declared!!!---- + No network, forcing cache access! + Please call RxNet.init() method to initialize in the project!!! + Starting background task...%d + Executing delete log files + Success + Failure + Stopping background task...%d + Single thread pool concurrent too high execution abandonment strategy + Glare parallelism is too high to execute abandonment strategy + :BufferedOutputStream execution time consuming: + milliseconds + text DES encryption after base64: + Unprocessed plaintext: + Decrypted data: + Downloading + Download cancelled + Traversal to the limit maximum number of layers + Directory contains non-standard files + The directory is empty + Filter to get mout table information: + Filter out mout information after subdirectory: + Filter out mout information after the same directory: + [Mount point device:%s;path:%s;size:%s;type:%s] + Ten thousand + Billion + Trillion + Formatting exception. + Ten thousand + Original: + Original rotation angle: + Width and height, + Quality before compression: + Compression parameters: + Compression image rotation angle: + Compression failed... + Compression image OOM + parseLong error + parseInt error + str2double error + str2double2len error + str2double0len error + parseFloat error + One + Two + Three + Four + Five + Six + Seven + Day + Below 6.0 + Below 6.0 + Below 6.0 + Above 6.0 and below 7.0 + Above 6.0 and below 7.0 + Above 6.0 and below 7.0 + Above 7.0 + Above 7.0 1 + 7.0 and above 1 + 7.0 and above 1 + 7.0 and above 2 + 7.0 and above 2 + 7.0 and above 2 + 7.0 and above 3 + 7.0 and above 3 + 7.0 and above 3 + Network exception + Cannot be null! + Failed to save obj + China Mobile + China Telecom + China Unicom + Unknown + Capricorn + Aquarius + Aquarius + Pisces + Pisces + Aries + Aries + Taurus + Taurus + Gemini + Gemini + Cancer + Cancer + Leo + Leo + Virgo + Virgo + Libra + Libra + Scorpio + Scorpio + Sagittarius + Sagittarius + Capricorn + During working hours + After working hours + During working hours + After working hours + Beta version + Built zip full path successfully: %s + Set bottom spacing + w + + Request failed, please try again later + Data parsing exception + Network abnormality, please check your network and try again + Parameter error + + diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml new file mode 100644 index 0000000..f7aa33c --- /dev/null +++ b/library/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/library/src/module_common/java/com/chwl/library/common/Constants.java b/library/src/module_common/java/com/chwl/library/common/Constants.java new file mode 100644 index 0000000..233ff0a --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/Constants.java @@ -0,0 +1,10 @@ +package com.chwl.library.common; + +public class Constants { + + //上传的图片 默认大小不能超过大小 640KB + public static final int UPLOAD_IMAGE_MAX_FILE_LENGTH = 640; + //上传的图片 默认宽高最大值 2340 + public static final int UPLOAD_IMAGE_MAX_SIZE = 2340; + public static final String GOOGLE = "google_molistar"; +} diff --git a/library/src/module_common/java/com/chwl/library/common/SpConstants.kt b/library/src/module_common/java/com/chwl/library/common/SpConstants.kt new file mode 100644 index 0000000..4c9eeae --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/SpConstants.kt @@ -0,0 +1,13 @@ +package com.chwl.library.common + +object SpConstants { + const val SET_PASSWORD = "set_password" + const val LOGIN_TYPE = "login_type" + const val GOOGLE = "google" + const val FACEBOOK = "facebook" + const val LINE = "line" + const val FAIRY_BUY_DEBRIS = "fairy_buy_debris" + const val FAIRY_BUY_SUCCESS = "fairy_buy_success" + const val BOX_BUY_LOVE = "box_buy_love" + const val BOX_BUY_SUCCESS = "box_buy_success" +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/application/BaseApp.java b/library/src/module_common/java/com/chwl/library/common/application/BaseApp.java new file mode 100644 index 0000000..5233a4f --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/application/BaseApp.java @@ -0,0 +1,41 @@ +package com.chwl.library.common.application; + +import android.app.Application; +import android.content.Context; + +import com.example.lib_utils.AppUtils; + +/** + * Application的代理类 + */ +public abstract class BaseApp extends Application{ + private static final String TAG = "BaseApp"; + public static Application gContext; + + /** + * @return 获取Application上下文对象 + */ + public static Context getContext() { + return gContext; + } + + /** + * @return 获取Application实例 + */ + public static Application getApplication() { + return gContext; + } + + public static void init(Application application) { + gContext = application; + AppUtils.init(application); + } + + /** + * debug 环境 受到实验室模式影响 + */ + public static boolean isDebug() { + return Env.isDebug(); + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/application/Env.java b/library/src/module_common/java/com/chwl/library/common/application/Env.java new file mode 100644 index 0000000..98f3071 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/application/Env.java @@ -0,0 +1,146 @@ +package com.chwl.library.common.application; + +import com.chwl.library.R; +import com.chwl.library.utils.ResUtil; +import com.chwl.library.utils.config.BasicConfig; +import com.chwl.library.utils.pref.CommonPref; + +/** + * 环境配置类 + */ +public class Env { + + public static final String KEY_ENVIRONMENT = "environment"; + + + + /** + * 当前环境 + */ + private static EnvType mEnvType; + /** + * 真实环境 + */ + private static boolean mRealDebug; + + + private Env() { + + } + + + public enum EnvType { + /** + * 测试环境 + */ + Debug(0), + /** + * 待发布环境 + */ + Staging(1), + /** + * 线上环境 + */ + Release(2); + + public int code; + + EnvType(int code) { + this.code = code; + } + + + public static EnvType create(int code) { + EnvType env = null; + if (code == EnvType.Debug.code) { + env = Debug; + } else if (code == EnvType.Staging.code) { + env = Staging; + } else if (code == EnvType.Release.code) { + env = Release; + } + return env; + } + } + + /** + * 初始化环境参数 + * + * @param defaultEnv 用于初始化最初环境 + * @param isRealDebug 判断是否是真的debug模式,不受环境影响 + */ + public static void initEnv(String defaultEnv, boolean isRealDebug) { + if (defaultEnv == null || defaultEnv.isEmpty()) { + throw new RuntimeException(ResUtil.getString(R.string.yizhuan_xchat_android_core_env_01)); + } + int environment = CommonPref.instance(BasicConfig.INSTANCE.getAppContext()).getInt(KEY_ENVIRONMENT); + EnvType envType; + if (environment == -1) { + envType = EnvType.valueOf(firstChar2Up(defaultEnv)); + changeEnv(envType); + } else { + envType = EnvType.create(environment); + } + if (envType == null) { + throw new RuntimeException(ResUtil.getString(R.string.yizhuan_xchat_android_core_env_02)); + } + + mEnvType = envType; + mRealDebug = isRealDebug; + } + + /** + * 第一个字符大写,用于把字符串转成类名 + * + * @param s + * @return + */ + private static String firstChar2Up(String s) { + if (s == null || s.length() < 1) { + return null; + } + + String newStr = s.substring(0, 1).toUpperCase() + s.substring(1); + + return newStr; + } + + /** + * 修改偏好设置里面的值 + * + * @param env + */ + public static void changeEnv(EnvType env) { + CommonPref.instance(BasicConfig.INSTANCE.getAppContext()).putInt(KEY_ENVIRONMENT, env.code); + } + + /** + * 受到环境印象(和实验室有关) + * + * @return + */ + public static boolean isDebug() { + return mEnvType == EnvType.Debug && mRealDebug; + } + + /** + * 真实包环境(不受实验室环境影响) + * + * @return + */ + public static boolean isRealDebug() { + return mRealDebug; + } + + /** + * 获取当前环境 + * @return + */ + public static EnvType getCurrentEnv() { + if (mEnvType == null) { + throw new RuntimeException(ResUtil.getString(R.string.yizhuan_xchat_android_core_env_03)); + } + return mEnvType; + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/application/IAppLifeCycle.java b/library/src/module_common/java/com/chwl/library/common/application/IAppLifeCycle.java new file mode 100644 index 0000000..6af36d4 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/application/IAppLifeCycle.java @@ -0,0 +1,12 @@ +package com.chwl.library.common.application; + +import android.app.Application; +import android.content.Context; +import android.content.res.Configuration; + +public interface IAppLifeCycle { + + void init(Application application); + +} + diff --git a/library/src/module_common/java/com/chwl/library/common/base/BaseActivity.java b/library/src/module_common/java/com/chwl/library/common/base/BaseActivity.java new file mode 100644 index 0000000..c73a00f --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/base/BaseActivity.java @@ -0,0 +1,211 @@ +package com.chwl.library.common.base; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.viewbinding.ViewBinding; +import java.lang.reflect.Field; +import java.util.List; + +import com.chwl.library.common.fragmentation.ISupportActivity; +import com.chwl.library.common.fragmentation.SupportActivityDelegate; +import com.chwl.library.common.util.LibLogger; +import com.chwl.library.common.util.ViewBindingUtil; + +public abstract class BaseActivity extends AppCompatActivity implements ISupportActivity { + private final String TAG = getClass().getSimpleName(); + private final SupportActivityDelegate mActivityDelegate = new SupportActivityDelegate(this); + @Nullable + private VB mViewBinding; + + @CallSuper + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + try { + super.onActivityResult(requestCode, resultCode, data); + this.handleFragmentActivityResult(this.getSupportFragmentManager(), requestCode, resultCode, data); + } catch (Exception e) { + LibLogger.error(TAG, "onActivityResult", e); + } + } + + private void handleFragmentActivityResult(FragmentManager fragmentManager, int requestCode, int resultCode, Intent data) { + if (fragmentManager != null) { + List fragmentList = fragmentManager.getFragments(); + int size = fragmentList.size(); + if (size > 0) { + for (int i = 0; i < size; i++) { + Fragment fragment = fragmentList.get(i); + if (fragment != null) { + fragment.onActivityResult(requestCode, resultCode, data); + this.handleFragmentActivityResult(fragment.getChildFragmentManager(), requestCode, resultCode, data); + } + } + } + } + } + + @CallSuper + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.mActivityDelegate.onCreate(); + this.initBefore(savedInstanceState); + mViewBinding = ViewBindingUtil.inflateWithActivity(this, getLayoutInflater()); + if (mViewBinding != null) { + View mContentView = mViewBinding.getRoot(); + mContentView.setTag(BaseViewTag.TAG_NAME, this); + setContentView(mContentView); + } + this.findView(); + this.setView(); + this.setListener(); + } + + @CallSuper + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + /** + * 建议子类相关操作写在super.onResume()之前 + */ + @CallSuper + @Override + protected void onResume() { + try { + super.onResume(); + } catch (Exception e) { + callUpActivity(); + LibLogger.error(TAG, "onResume", e); + } + } + + /** + * 解决onResume()不明原因导致的java.lang.IllegalArgumentException + * 参考文档:https://blog.csdn.net/ahubenkui/article/details/80038381 + */ + private void callUpActivity() { + try { + Class superClass = Activity.class; + Field field = superClass.getDeclaredField("mCalled"); + field.setAccessible(true); + field.setBoolean(this, true); + } catch (Exception e) { + LibLogger.error(TAG, "callUpActivity", e); + } + } + + /** + * 建议子类相关操作写在super.onPause()之前 + */ + @CallSuper + @Override + protected void onPause() { + super.onPause(); + //当isFinishing()返回为true时,表示当前Activity将要销毁,但最终不会走Activity#onDestroy()方法。 + if (this.isFinishing()) { + this.onWillDestroy(); + } + } + + @CallSuper + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + try { + super.onSaveInstanceState(outState); + } catch (Exception e) { + LibLogger.error(TAG, "onSaveInstanceState", e); + } + } + + /** + * 不建议子类在Activity#onDestroy()方法里面做各种释放操作,因为onDestroy()方法不一定会执行,有可能会导致内存泄露,请使用:onWillDestroy()方法。 + * 参考文档:https://blog.csdn.net/wangsf1112/article/details/79108856 + */ + @CallSuper + @Override + protected void onDestroy() { + super.onDestroy(); + this.onWillDestroy(); + } + + /** + * 建议子类各种释放操作,放到onWillDestroy()中方法执行,因为Activity#onDestroy()方法不一定会执行,有可能会导致内存泄露。 + */ + protected void onWillDestroy() { + + } + + @Override + public final SupportActivityDelegate getSupportDelegate() { + return this.mActivityDelegate; + } + + @Override + public final void onBackPressed() { + this.mActivityDelegate.onBackPressedPage(); + } + + @Override + public void onBackPressedSupport() { + this.mActivityDelegate.onBackPressedSupport(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + try { + return super.dispatchTouchEvent(ev); + } catch (Exception e) { + LibLogger.error(TAG, "dispatchTouchEvent", e); + return false; + } + } + + @Nullable + protected final VB getBinding() { + return mViewBinding; + } + + ////////////////////////////////////////以下是提供给子类复写的方法//////////////////////////////////////// + + /** + * 该方法是在onCreate()方法里执行,在setContentView()方法被调用之前触发,可用于处理解析Activity#getIntent()中的数据时的场景 + */ + protected void initBefore(@Nullable Bundle savedInstanceState) { + + } + + /** + * 该方法是在onCreate()方法里执行,在setContentView()方法被调用之后触发,可用于处理控件的初始化 + */ + protected void findView() { + + } + + /** + * 该方法是在onCreate()方法里执行,在setContentView()方法被调用之后触发,可用于处理控件的加载数据 + */ + protected void setView() { + + } + + /** + * 该方法是在onCreate()方法里执行,在setContentView()方法被调用之后触发,可用于处理控件的设置监听器 + */ + protected void setListener() { + + } + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/base/BaseCommonDialogFragment.java b/library/src/module_common/java/com/chwl/library/common/base/BaseCommonDialogFragment.java new file mode 100644 index 0000000..2b56e5d --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/base/BaseCommonDialogFragment.java @@ -0,0 +1,234 @@ +package com.chwl.library.common.base; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import androidx.annotation.CallSuper; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.viewbinding.ViewBinding; + +import com.chwl.library.common.fragmentation.ISupportActivity; +import com.chwl.library.common.fragmentation.ISupportFragment; +import com.chwl.library.common.fragmentation.SupportFragmentDelegate; +import com.chwl.library.common.fragmentation.windowcallback.WindowCallbackProxyUtil; +import com.chwl.library.common.util.ActivityHelper; +import com.chwl.library.common.util.LibLogger; +import com.chwl.library.common.util.ViewBindingUtil; + +public abstract class BaseCommonDialogFragment extends DialogFragment implements ISupportFragment { + private final String TAG = getClass().getSimpleName(); + private final SupportFragmentDelegate mFragmentDelegate = new SupportFragmentDelegate(this); + private View mContentView; + @Nullable + private VB mViewBinding; + private boolean isLoaded = false; + + @Override + public void onStart() { + super.onStart(); + if (getDialog() != null && getDialog().getWindow() != null) { + Window window = getDialog().getWindow(); + Window.Callback callback = WindowCallbackProxyUtil.createWindowCallBack(window.getCallback(), true, this.getClass().getSimpleName()); + if (callback != null) { + window.setCallback(callback); + } + } + } + + @CallSuper + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getActivity() == null) { + //这里需要关联一个Activity而不是Context + throw new IllegalStateException("Current class " + getClass().getName() + " not attached to an activity."); + } + if (!(getActivity() instanceof ISupportActivity)) { + //Activity必须是实现了ISupportActivity接口才可以 + throw new IllegalStateException(getActivity().getClass().getName() + " must impl ISupportActivity! Current class is " + getClass().getName()); + } + this.mFragmentDelegate.onAttach(); + } + + @CallSuper + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.mFragmentDelegate.onCreate(savedInstanceState); + this.initBefore(savedInstanceState); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mViewBinding = ViewBindingUtil.inflateWithFragment(this, inflater, container); + if (mViewBinding != null) { + this.mContentView = mViewBinding.getRoot(); + this.mContentView.setTag(BaseViewTag.TAG_NAME, this); + } + this.findView(); + return this.mContentView; + } + + @CallSuper + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + this.mFragmentDelegate.onViewCreated(); + this.setView();//重写该方法即可实现预加载数据,即当Fragment创建布局以后还没有真正可见时就会加载数据,不同与onResume()方法和onLazyLoad()方法 + this.setListener(); + } + + @CallSuper + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + this.mFragmentDelegate.onSaveInstanceState(outState); + } + + /** + * 重写该方法即可实现每次Fragment可见时都能加载数据,不同与setView()方法和onLazyLoad()方法 + */ + @CallSuper + @Override + public void onResume() { + super.onResume(); + if (!isLoaded && !isHidden()) { + onLazyLoad(); + isLoaded = true; + } + } + + /** + * 重写该方法可实现懒加载数据,即当前Fragment第一次可见时才会执行一次,不同与setView()方法和onResume()方法 + */ + protected void onLazyLoad() { + + } + + /** + * 重写该方法即可监听当前Fragment变为不可见时 + */ + @CallSuper + @Override + public void onPause() { + super.onPause(); + } + + @CallSuper + @Override + public void onDestroyView() { + super.onDestroyView(); + this.mFragmentDelegate.onDestroyView(); + isLoaded = false; + this.onWillDestroy(); + } + + /** + * 建议子类各种释放操作,放到onWillDestroy()方法中执行,因为Fragment#onDestroyView()方法一定会执行所以也会触发该方法,在其他方法中处理有可能会导致内存泄露。 + */ + @CallSuper + public void onWillDestroy() { + this.mContentView = null; + } + + @CallSuper + @Override + public void show(@NonNull FragmentManager manager, @Nullable String tag) { + try { + if (!manager.isDestroyed() && !manager.isStateSaved() && !isAdded()) { + super.show(manager, tag); + } + } catch (Exception e) { + LibLogger.error(TAG, "show", e); + } + } + + @Override + public final void dismiss() { + dismissAllowingStateLoss(); + } + + @Override + public final void dismissAllowingStateLoss() { + if (ActivityHelper.isCanUse(getActivity())) { + super.dismissAllowingStateLoss(); + } + } + + @Nullable + @Override + public final View getView() { + return this.mContentView; + } + + protected final T findViewById(@IdRes int id) { + if (this.getView() != null) { + return this.getView().findViewById(id); + } + return null; + } + + @Override + public final SupportFragmentDelegate getSupportDelegate() { + return this.mFragmentDelegate; + } + + @Override + public final void setFragmentResult(int resultCode, Bundle bundle) { + this.mFragmentDelegate.setFragmentResult(resultCode, bundle); + } + + @Override + public void onFragmentResult(int requestCode, int resultCode, Bundle data) { + this.mFragmentDelegate.onFragmentResult(requestCode, resultCode, data); + } + + @Override + public boolean onBackPressedSupport() { + return this.mFragmentDelegate.onBackPressedSupport(); + } + + @Nullable + protected final VB getBinding() { + return mViewBinding; + } + + ////////////////////////////////////////以下是提供给子类复写的方法//////////////////////////////////////// + + /** + * 该方法是在onCreate()方法里执行,在onCreateView()方法被调用之前触发,可用于处理解析Fragment#getArguments()中的数据时的场景 + */ + protected void initBefore(@Nullable Bundle savedInstanceState) { + + } + + /** + * 该方法是在onCreateView()方法里触发,可用于处理控件的初始化 + */ + protected void findView() { + + } + + /** + * 该方法是在onViewCreated()方法里触发,重写该方法即可实现预加载数据,即当Fragment创建布局以后还没有真正可见时就会加载数据,不同与onResume()方法和onLazyLoad()方法 + */ + protected void setView() { + + } + + /** + * 该方法是在onViewCreated()方法里触发,可用于处理控件的设置监听器 + */ + protected void setListener() { + + } + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/base/BaseLinearLayout.java b/library/src/module_common/java/com/chwl/library/common/base/BaseLinearLayout.java new file mode 100644 index 0000000..9c2a138 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/base/BaseLinearLayout.java @@ -0,0 +1,165 @@ +package com.chwl.library.common.base; + +import android.app.Activity; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.LinearLayout; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.viewbinding.ViewBinding; + +import com.chwl.library.common.util.ActivityHelper; +import com.chwl.library.common.util.ViewBindingUtil; + +public abstract class BaseLinearLayout extends LinearLayout implements DefaultLifecycleObserver { + private boolean mViewCreatedCalled = false; + @Nullable + private VB mViewBinding; + + public BaseLinearLayout(Context context) { + this(context, null); + } + + public BaseLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseLinearLayout(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } + + public BaseLinearLayout(Context context, AttributeSet attrs, int defStyle, int defStyleRes) { + super(context, attrs, defStyle, defStyleRes); + init(); + } + + private void init() { + mViewBinding = ViewBindingUtil.inflateWithView(this, LayoutInflater.from(getContext()), this,true); + BaseViewTag.registerLifecycle(this); + findView(); + } + + @CallSuper + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!mViewCreatedCalled) { + onLazyLoad(); + mViewCreatedCalled = true; + } + } + + /** + * 重写该方法可实现懒加载数据,即当前控件第一次可见时才会执行一次,不同于在构造函数里面使用预加载机制直接加载数据的方式 + */ + @CallSuper + protected void onLazyLoad() { + setView(); + setListener(); + } + + public final Activity getActivity() { + return ActivityHelper.getActivityFromView(this); + } + + @Nullable + protected final VB getBinding() { + return mViewBinding; + } + + /** + * 来自于Activity或Fragment的onCreate()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onCreate(@NonNull LifecycleOwner owner) { + + } + + /** + * 来自于Activity或Fragment的onStart()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onStart(@NonNull LifecycleOwner owner) { + + } + + /** + * 来自于Activity或Fragment的onResume()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onResume(@NonNull LifecycleOwner owner) { + + } + + /** + * 来自于Activity或Fragment的onPause()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onPause(@NonNull LifecycleOwner owner) { + + } + + /** + * 来自于Activity或Fragment的onStop()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onStop(@NonNull LifecycleOwner owner) { + + } + + /** + * 来自于Activity或Fragment的onDestroy()方法被回调 + * + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + @CallSuper + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + BaseViewTag.unRegisterLifecycle(this, owner); + } + + + ////////////////////////////////////////以下是提供给子类复写的方法//////////////////////////////////////// + + /** + * 该方法是在构造函数里执行,可用于处理控件的初始化 + */ + protected void findView() { + + } + + /** + * 该方法是在onLazyLoad()方法里执行,可用于处理控件的加载数据 + */ + protected void setView() { + + } + + /** + * 该方法是在onLazyLoad()方法里执行,可用于处理控件的设置监听器 + */ + protected void setListener() { + + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/base/BaseViewTag.java b/library/src/module_common/java/com/chwl/library/common/base/BaseViewTag.java new file mode 100644 index 0000000..1b2adea --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/base/BaseViewTag.java @@ -0,0 +1,119 @@ +package com.chwl.library.common.base; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import com.chwl.library.R; + +import com.chwl.library.common.util.LibLogger; + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc:将View关联上Activity或者Fragment的生命周期 + */ +public class BaseViewTag { + private static final String TAG = "BaseViewTag"; + public static final int TAG_NAME = R.id.baseViewTagID; + + /** + * 注册Activity或者Fragment生命周期监听接口 + * + * @param view 需要关联Activity或者Fragment生命周期的控件,注意该控件需要实现LifecycleObserver接口 + */ + public static void registerLifecycle(View view) { + if (view == null) { + LibLogger.warn(TAG, "registerLifecycle view is null"); + return; + } + if (!(view instanceof LifecycleObserver)) { + LibLogger.warn(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", view must implements LifecycleObserver"); + return; + } + view.post(new Runnable() { + @Override + public void run() { + try { + Object tag = view.getTag(BaseViewTag.TAG_NAME); + if (tag != null) { + register(view, tag); + return; + } + ViewParent viewParent = view.getParent(); + ViewGroup parentView = (ViewGroup) viewParent; + while (parentView != null) { + tag = parentView.getTag(BaseViewTag.TAG_NAME); + if (tag instanceof LifecycleOwner) { + register(view, tag); + return; + } + viewParent = parentView.getParent(); + if (viewParent instanceof ViewGroup) { + parentView = (ViewGroup) viewParent; + } else { + break; + } + } + register(view, view.getContext()); + } catch (Exception e) { + LibLogger.error(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", error", e); + } + } + }); + } + + /** + * 真正将view关联到Activity或者Fragment的生命周期 + * + * @param view 需要关联Activity或者Fragment生命周期的控件,注意该控件需要实现LifecycleObserver接口 + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + private static void register(View view, Object owner) { + if (view == null) { + LibLogger.warn(TAG, "registerLifecycle view is null"); + return; + } + if (owner == null) { + LibLogger.warn(TAG, "registerLifecycle owner is null"); + return; + } + try { + if (owner instanceof LifecycleOwner) { + LifecycleOwner lifecycleOwner = (LifecycleOwner) owner; + lifecycleOwner.getLifecycle().addObserver((LifecycleObserver) view);//核心代码 + view.setTag(BaseViewTag.TAG_NAME, owner); + LibLogger.debug(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName()); + } else { + LibLogger.warn(TAG, "registerLifecycle view:" + view.getClass().getSimpleName() + ", owner is not instanceof LifecycleOwner"); + } + } catch (Exception e) { + LibLogger.error(TAG, "registerLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName() + ", error", e); + } + } + + /** + * 解绑Activity或者Fragment生命周期监听接口 + * + * @param view 需要关联Activity或者Fragment生命周期的控件,注意该控件需要实现LifecycleObserver接口 + * @param owner 生命周期提供者,一般是Activity或Fragment + */ + public static void unRegisterLifecycle(View view, LifecycleOwner owner) { + if (view == null) { + LibLogger.warn(TAG, "unRegisterLifecycle view is null"); + return; + } + if (owner == null) { + LibLogger.warn(TAG, "unRegisterLifecycle owner is null"); + return; + } + try { + view.setTag(BaseViewTag.TAG_NAME, null); + owner.getLifecycle().removeObserver((LifecycleObserver) view); + LibLogger.debug(TAG, "unRegisterLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName()); + } catch (Exception e) { + LibLogger.error(TAG, "unRegisterLifecycle view: " + view.getClass().getSimpleName() + ", owner: " + owner.getClass().getSimpleName() + ", error", e); + } + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/delegate/SpDelegate.kt b/library/src/module_common/java/com/chwl/library/common/delegate/SpDelegate.kt new file mode 100644 index 0000000..66f6b71 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/delegate/SpDelegate.kt @@ -0,0 +1,50 @@ +package com.chwl.library.common.delegate + +import com.chwl.library.common.application.BaseApp +import com.chwl.library.common.util.SPUtils +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: sp存储和取出委托 + */ +class SpDelegate(private val key: String, private val default: T) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val value = when (default) { + is Boolean -> SPUtils.getBoolean(key, default) + is String -> SPUtils.getString(key, default) + is Long -> SPUtils.getLong(key, default) + is Int -> SPUtils.getInt(key, default) + is Float -> SPUtils.getFloat(key, default) + is Double -> SPUtils.getDouble(key, default) + is ByteArray -> SPUtils.getBytes(key, default) + else -> { + if (BaseApp.isDebug()) { + throw IllegalArgumentException("SpDelegate: this type is no supported") + } else { + null + } + } + } + return (value as? T) ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + when (value) { + is Boolean -> SPUtils.putBoolean(key, value) + is String -> SPUtils.putString(key, value) + is Long -> SPUtils.putLong(key, value) + is Int -> SPUtils.putInt(key, value) + is Float -> SPUtils.putFloat(key, value) + is Double -> SPUtils.putDouble(key, value) + is ByteArray -> SPUtils.putBytes(key, value) + else -> { + if (BaseApp.isDebug()) { + throw IllegalArgumentException("SpDelegate: this type is no supported") + } + } + } + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/entity/CommonTabEntity.kt b/library/src/module_common/java/com/chwl/library/common/entity/CommonTabEntity.kt new file mode 100644 index 0000000..a9981f1 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/entity/CommonTabEntity.kt @@ -0,0 +1,10 @@ +package com.chwl.library.common.entity + +import androidx.fragment.app.Fragment + +/** + * author: wushaocheng + * time: 2022/2/16 + * desc: 公共tab + */ +class CommonTabEntity(val frgClazz: Class, val title: String) \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/file/FileHelper.java b/library/src/module_common/java/com/chwl/library/common/file/FileHelper.java new file mode 100644 index 0000000..beac539 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/file/FileHelper.java @@ -0,0 +1,882 @@ +package com.chwl.library.common.file; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.text.TextUtils; + +import com.chwl.library.common.application.BaseApp; +import com.chwl.library.common.util.LibLogger; +import com.chwl.library.utils.FP; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * 文件工具类 + */ +public class FileHelper { + private static final String TAG = "FileHelper"; + private static final String NO_MEDIA = ".nomedia"; + private static File rootCacheDir; + private static HashMap rootFileDirMap = new HashMap<>(); + private static final String[] mCs = new String[]{"/", "\\", "?", "*", ":", "<", ">", "|", "\""}; + private static final char UNICODE_SURROGATE_START_CHAR = '\ud800'; + private static final char UNICODE_SURROGATE_END_CHAR = '\udfff'; + + /** + * 创建.nomedia文件 + * + * @param parentFile 上级目录 + */ + private static void createNoMediaFile(File parentFile) { + File no_media = new File(parentFile, NO_MEDIA); + if (!no_media.exists()) { + try { + no_media.createNewFile(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 获取Android/data/当前应用包名/cache文件夹 + * + * @return 当前应用缓存根目录 + */ + public static File getRootCacheDir() { + if (rootCacheDir != null) { + //因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + return rootCacheDir; + } + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File file = BaseApp.getContext().getExternalCacheDir(); + if (file != null) { + createNoMediaFile(file); + //因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + rootCacheDir = file; + return file; + } + } + File file = BaseApp.getContext().getCacheDir(); + createNoMediaFile(file); + //因为频繁调用getExternalCacheDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + rootCacheDir = file; + return file; + } + + /** + * 获取Android/data/当前应用包名/files文件夹 + * + * @param type 类型从以下方式中选择,也可以为null: + * {@link Environment#DIRECTORY_MUSIC}, + * {@link Environment#DIRECTORY_PODCASTS}, + * {@link Environment#DIRECTORY_RINGTONES}, + * {@link Environment#DIRECTORY_ALARMS}, + * {@link Environment#DIRECTORY_NOTIFICATIONS}, + * {@link Environment#DIRECTORY_PICTURES}, + * {@link Environment#DIRECTORY_MOVIES}, + * {@link Environment#DIRECTORY_DOWNLOADS}, + * {@link Environment#DIRECTORY_DCIM}, + * {@link Environment#DIRECTORY_DOCUMENTS}, + * {@link Environment#DIRECTORY_AUDIOBOOKS} + * @return 当前应用文件根目录 + */ + public static File getRootFilesDir(@androidx.annotation.Nullable String type) { + String dirName = (type != null && type.length() > 0) ? type.trim() : null; + if (TextUtils.isEmpty(dirName)) { + //因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + File file = rootFileDirMap.get("empty"); + if (file != null) { + return file; + } + } else { + //因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + File file = rootFileDirMap.get(dirName); + if (file != null) { + return file; + } + } + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File file = BaseApp.getContext().getExternalFilesDir(dirName); + if (file != null) { + createNoMediaFile(file); + //因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + if (TextUtils.isEmpty(dirName)) { + rootFileDirMap.put("empty", file); + } else { + rootFileDirMap.put(dirName, file); + } + return file; + } + } + File file = BaseApp.getContext().getFilesDir(); + createNoMediaFile(file); + //因为频繁调用getExternalFilesDir方法会在某些机器上面出现ANR,所以这里降低频率调用。 + rootFileDirMap.put("empty", file); + return file; + } + + /** + * 创建临时文件 + * + * @param dir 文件夹 + * @return 临时文件 + */ + public static File createTempFile(File dir) { + try { + File file = File.createTempFile("pic", null, dir); + file.deleteOnExit(); + return file; + } catch (IOException e) { + return null; + } + } + + /** + * 判断文件或文件夹是否存在 + * + * @param filePath 文件路径 + * @return 是否存在这个文件或文件夹 + */ + public static boolean isFileExist(String filePath) { + if (TextUtils.isEmpty(filePath)) { + return false; + } + + File file = new File(filePath); + return file.exists(); + } + + /** + * 确保文件夹存在,不存在的时候就创建该目录 + * + * @param dirPath 文件夹路径 + * @return 确保是否已存在这个文件夹 + */ + public static boolean ensureDirExists(String dirPath) { + File dirFile = new File(dirPath); + if (!dirFile.exists()) { + return dirFile.mkdirs(); + } + return true; + } + + /** + * 确保该文件的文件夹存在,不存在的时候就创建该文件的文件夹 + * + * @param filePath 文件路径 + * @return 确保是否已存在该文件的文件夹 + */ + public static boolean ensureFileDirExists(String filePath) { + String dir = getDirOfFilePath(filePath); + if (TextUtils.isEmpty(dir)) { + return false; + } + ensureDirExists(dir); + return true; + } + + /** + * 确保该文件存在,不存在的时候就创建该文件 + * + * @param filePath 文件路径 + * @return 确保是否已存在该文件 + */ + public static File ensureFileExists(String filePath) { + if (!ensureFileDirExists(filePath)) { + return null; + } + File file = new File(filePath); + if (file.exists()) { + return file; + } + try { + if (!file.exists() && !file.createNewFile()) { + file = null; + } + } catch (IOException e) { + e.printStackTrace(); + file = null; + } + return file; + } + + /** + * 从文件路径里提取文件夹的路径 + * + * @param filePath 文件路径 + * @return 文件夹的路径 + */ + public static String getDirOfFilePath(String filePath) { + if (TextUtils.isEmpty(filePath)) { + return null; + } + int sepPos = filePath.lastIndexOf(File.separatorChar); + if (sepPos == -1) { + return null; + } + return filePath.substring(0, sepPos); + } + + /** + * 删除单个文件 + * + * @param filePath 文件路径 + */ + public static void removeFile(String filePath) { + if (!TextUtils.isEmpty(filePath)) { + try { + File file = new File(filePath); + file.delete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 删除文件或文件夹下面所有的文件 + * + * @param filePath 文件路径 + * @return 是否删除成功 + */ + public static boolean removeAllFile(String filePath) { + if (TextUtils.isEmpty(filePath)) { + return true; + } + + File file = new File(filePath); + if (!file.exists()) { + return true; + } + if (file.isFile()) { + try { + return file.delete(); + } catch (Exception e) { + return false; + } + } + if (!file.isDirectory()) { + return false; + } + File[] files = file.listFiles(); + if (files != null && files.length > 0) { + for (File f : files) { + if (f.isFile()) { + try { + f.delete(); + } catch (Exception e) { + e.printStackTrace(); + } + } else if (f.isDirectory()) { + removeAllFile(f.getAbsolutePath()); + } + } + } + try { + return file.delete(); + } catch (Exception e) { + return false; + } + } + + /** + * 将数据存储到缓存文件夹里 + * + * @param data 数据 + * @param filePath 文件路径 + */ + public static void saveByteArrayIntoFile(byte[] data, String filePath) { + try { + File f = new File(filePath); + if (f.createNewFile()) { + FileOutputStream fos = new FileOutputStream(f); + fos.write(data); + fos.flush(); + fos.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 将Bitmap保存为JPEG图片 + */ + public static void saveBitmapAsJPEG(Bitmap bmp, String filePath) { + if (bmp == null) { + return; + } + FileOutputStream fos = null; + try { + if (!ensureFileDirExists(filePath)) { + return; + } + File file = new File(filePath); + if (file.exists()) { + file.delete(); + } + fos = new FileOutputStream(file); + bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.flush(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (fos != null) { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 将Bitmap保存为PNG图片 + * + * @param bmp 图片 + * @param filePath 文件路径 + * @return 文件路径 + */ + public static String saveBitmapAsPNG(Bitmap bmp, String filePath) { + if (bmp == null) { + return ""; + } + FileOutputStream fos = null; + try { + if (!ensureFileDirExists(filePath)) { + return ""; + } + File file = new File(filePath); + if (file.exists()) { + file.delete(); + } + fos = new FileOutputStream(file); + bmp.compress(Bitmap.CompressFormat.PNG, 100, fos); + fos.flush(); + return filePath; + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (fos != null) { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return ""; + } + + /** + * 解压zip到指定的路径 + * + * @param zipFileString ZIP的名称 + * @param outPathString 要解压缩路径 + */ + public static boolean unzipFile(String zipFileString, String outPathString) { + ZipInputStream inZip = null; + try { + inZip = new ZipInputStream(new FileInputStream(zipFileString)); + ZipEntry zipEntry; + while ((zipEntry = inZip.getNextEntry()) != null) { + String szName = zipEntry.getName(); + if (szName.contains("/")) { + szName = szName.substring(szName.indexOf("/") + 1); + if (TextUtils.isEmpty(szName)) { + continue; + } + } + if (!zipEntry.isDirectory()) { + File file = new File(outPathString + File.separator + szName); + if (!file.exists()) { + if (file.getParentFile() != null) { + file.getParentFile().mkdirs(); + } + file.createNewFile(); + } + FileOutputStream out = new FileOutputStream(file); + int len; + byte[] buffer = new byte[1024]; + while ((len = inZip.read(buffer)) != -1) { + out.write(buffer, 0, len); + out.flush(); + } + out.close(); + } else { + szName = szName.substring(0, szName.length() - 1); + File folder = new File(outPathString + File.separator + szName); + folder.mkdirs(); + } + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + try { + if (inZip != null) { + inZip.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + + /** + * 解压压缩包 + * + * @param srcZipPath 原压缩包路径 + * @param destFilePath 目标文件夹路径 + * @return 解压之后的文件组 + * @throws IOException 抛出异常 + */ + public static File[] unzip(String srcZipPath, String destFilePath) throws IOException { + if (TextUtils.isEmpty(destFilePath)) { + throw new IOException(); + } + if (!destFilePath.endsWith(File.separator)) { + destFilePath = destFilePath + File.separator; + } + File destDir = new File(destFilePath); + if (!destDir.isDirectory() || !destDir.exists()) { + destDir.mkdir(); + } + ArrayList extractedFileList = new ArrayList<>(); + ZipInputStream inZip = null; + try { + ZipEntry zipEntry; + inZip = new ZipInputStream(new FileInputStream(srcZipPath)); + String szName; + while ((zipEntry = inZip.getNextEntry()) != null) { + szName = zipEntry.getName(); + if (zipEntry.isDirectory()) { + szName = szName.substring(0, szName.length() - 1); + File folder = new File(destDir.getAbsolutePath() + File.separator + szName); + folder.mkdirs(); + continue; + } + FileOutputStream out = null; + try { + int len; + File file = new File(destDir.getAbsolutePath() + File.separator + szName); + file = ensureFileExists(file.getAbsolutePath()); + if (file != null) { + out = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + while ((len = inZip.read(buffer)) != -1) { + out.write(buffer, 0, len); + out.flush(); + } + out.close(); + extractedFileList.add(file); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (out != null) { + out.close(); + } + } + } + } finally { + if (inZip != null) { + inZip.close(); + } + } + File[] extractedFiles = new File[extractedFileList.size()]; + extractedFileList.toArray(extractedFiles); + return extractedFiles; + } + + /** + * 从文件路径里面提取文件的后缀 + * + * @param filePath 文件路径 + * @return 后缀,如.mp3 + */ + public static String getFileExtension(String filePath) { + String fileName = getFileName(filePath); + if (TextUtils.isEmpty(fileName)) { + return null; + } + int index = fileName.lastIndexOf("."); + if (index != -1) { + return fileName.substring(index); + } + return null; + } + + /** + * 从文件路径里面提取文件名 + * + * @param filePath 文件路径 + * @return 文件名称 + */ + public static String getFileName(String filePath) { + if (filePath != null) { + String slash = "/"; + int pos = filePath.lastIndexOf(slash) + 1; + if (pos > 0) { + String name = filePath.substring(pos); + if (!TextUtils.isEmpty(name)) { + name = name.replace("?", ""); + } + return name; + } + } + return null; + } + + /** + * 从URL里面提取版本名 + * + * @param url 链接 + * @return 文件名 + */ + public static String getFileNameWithVer(String url) { + String versionName = ""; + String subName = getFileName(url); + if (subName != null && subName.contains("?")) { + String[] tempArr = subName.split("\\?"); + if (tempArr.length > 1) { + subName = tempArr[0]; + versionName = tempArr[1]; + } + } + + String fileName = dropExt(subName); + fileName = fileName + versionName; + return fileName; + } + + /** + * 从文件名里面踢出点得到可用的文件名 + * + * @param fileName 文件名 + * @return 文件名 + */ + public static String dropExt(String fileName) { + if (!TextUtils.isEmpty(fileName)) { + int pos = fileName.lastIndexOf("."); + if (pos != -1) { + return FP.take(pos, fileName); + } + } + return fileName; + } + + /** + * 读取文件内容转换为字符串 + * + * @param filePath 文件路径 + */ + public static String getStringFromFile(String filePath) { + String result = ""; + InputStream is = null; + try { + is = new FileInputStream(filePath); + int length = is.available(); + byte[] buffer = new byte[length]; + is.read(buffer); + result = new String(buffer, StandardCharsets.UTF_8); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } + + /** + * 读取Assets内容转换为字符串 + * + * @param fileName 文件名 + * @return 字符串内容 + */ + public static String getStringFromAssets(String fileName) { + String result = ""; + try { + InputStream is = BaseApp.getContext().getAssets().open(fileName); + int length = is.available(); + byte[] buffer = new byte[length]; + is.read(buffer); + result = new String(buffer, StandardCharsets.UTF_8); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + /** + * 从Assets里面读取内容覆盖本地文件 + * + * @param dir 文件夹路径 + * @param fileName 文件名称 + * @param overwrite 是否覆盖,即删除旧文件重新创建新文件 + * @return 操作是否成功 + */ + public static boolean copyFileFromAssets(String dir, String fileName, boolean overwrite) { + String path = dir + File.separator + fileName; + File file = new File(path); + if (file.exists() && overwrite) { + file.delete(); + } + if (!file.exists()) { + try { + if (!ensureDirExists(file.getParentFile().getAbsolutePath())) { + return false; + } + file.createNewFile(); + InputStream in = BaseApp.getContext().getAssets().open(fileName); + OutputStream out = new FileOutputStream(file); + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) > 0) { + out.write(buffer, 0, n); + } + out.flush(); + in.close(); + out.close(); + } catch (Exception e) { + try { + file.delete(); + } catch (Exception ex) { + ex.printStackTrace(); + } + return false; + } + } + return true; + } + + /** + * 将字节数组写入文件中 + * + * @param buffer 字节数组 + * @param folderPath 文件夹路径 + * @param fileName 文件名称 + */ + public static void saveDataToFile(byte[] buffer, String folderPath, String fileName) { + File fileDir = new File(folderPath); + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + + File file = new File(folderPath + File.separator + fileName); + FileOutputStream out = null; + try { + out = new FileOutputStream(file); + out.write(buffer); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 格式化大小,带单位 + */ + public static String formatSize(long size) { + //获取到的size为:1705230 + int GB = 1024 * 1024 * 1024;//定义GB的计算常量 + int MB = 1024 * 1024;//定义MB的计算常量 + int KB = 1024;//定义KB的计算常量 + DecimalFormat df = new DecimalFormat("0.0");//格式化小数 + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + String resultSize = ""; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = df.format(size / (float) GB) + "GB"; + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = df.format(size / (float) MB) + "MB"; + } else if (size / KB >= 1) { + //如果当前Byte的值大于等于1KB + resultSize = df.format(size / (float) KB) + "KB"; + } else { + resultSize = size + "B"; + } + return resultSize; + } + + /** + * @param fileName 文件名 + * @return 文件名是否正确 + */ + public static boolean isFileNameCorrect(String fileName) { + if (null == fileName) { + return false; + } else { + fileName = fileName.trim(); + if (fileName.length() == 0) { + return false; + } else { + for (String c : mCs) { + if (fileName.contains(c)) { + return false; + } + } + return !containsSurrogateChar(fileName); + } + } + } + + /** + * @param string 字符串 + * @return 是否包含需要替代的字符串 + */ + public static boolean containsSurrogateChar(String string) { + if (TextUtils.isEmpty(string)) { + return false; + } else { + int length = string.length(); + boolean hasSurrogateChar = false; + for (int i = 0; i < length; ++i) { + char c = string.charAt(i); + if (UNICODE_SURROGATE_START_CHAR <= c && c <= UNICODE_SURROGATE_END_CHAR) { + hasSurrogateChar = true; + break; + } + } + return hasSurrogateChar; + } + } + + /** + * 把Uri转换为文件 + * + * @param uri 源文件的Uri的路径 + * @param path 要存入的文件的路径 + * @param overwrite 是否需要复写 + * @return 是否已经把Uri转换成功为文件了 + */ + public static boolean copyFileFromUri(Uri uri, String path, boolean overwrite) { + File file = new File(path); + if (file.exists() && overwrite) { + file.delete(); + } + if (!file.exists()) { + try { + if (!FileHelper.ensureDirExists(file.getParentFile().getAbsolutePath())) { + return false; + } + InputStream stream = BaseApp.getContext().getContentResolver().openInputStream(uri); + if (stream != null) { + file.createNewFile(); + OutputStream out = new FileOutputStream(file); + byte buffer[] = new byte[4096]; + int n; + while ((n = stream.read(buffer)) > 0) { + out.write(buffer, 0, n); + } + out.flush(); + stream.close(); + out.close(); + } + } catch (Exception e) { + try { + file.delete(); + } catch (Exception ex) { + ex.printStackTrace(); + } + return false; + } + } + return true; + } + + /** + * 将文件转换成字节数组 + * + * @param file 文件 + * @return 字节数组 + */ + public static byte[] fileToByteArray(File file) { + if (file.exists() && file.canRead()) { + try { + return streamToBytes(new FileInputStream(file)); + } catch (Exception e) { + LibLogger.error(TAG, String.valueOf(e)); + } + } + return null; + } + + /** + * 将文件流转换成字节数组 + * + * @param inputStream 输入流 + * @return 字节数组 + */ + public static byte[] streamToBytes(InputStream inputStream) { + byte[] content = null; + ByteArrayOutputStream baos = null; + BufferedInputStream bis = null; + try { + baos = new ByteArrayOutputStream(); + bis = new BufferedInputStream(inputStream); + byte[] buffer = new byte[1024]; + int length; + while ((length = bis.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + content = baos.toByteArray(); + if (content.length == 0) { + content = null; + } + } catch (IOException e) { + LibLogger.error(TAG, String.valueOf(e)); + } finally { + if (baos != null) { + try { + baos.close(); + } catch (IOException e) { + LibLogger.error(TAG, String.valueOf(e)); + } + } + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + LibLogger.error(TAG, String.valueOf(e)); + } + } + } + return content; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportActivity.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportActivity.java new file mode 100644 index 0000000..5911770 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportActivity.java @@ -0,0 +1,13 @@ +package com.chwl.library.common.fragmentation; + +/** + * Created by YoKey on 17/6/13. + */ +public interface ISupportActivity { + SupportActivityDelegate getSupportDelegate(); + + void onBackPressedPage(); + + void onBackPressedSupport(); + +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportFragment.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportFragment.java new file mode 100644 index 0000000..1d82d12 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/ISupportFragment.java @@ -0,0 +1,36 @@ +package com.chwl.library.common.fragmentation; + +import android.os.Bundle; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by YoKey on 17/6/23. + */ + +public interface ISupportFragment { + // LaunchMode + int STANDARD = 0; + int SINGLETOP = 1; + int SINGLETASK = 2; + + // ResultCode + int RESULT_CANCELED = 0; + int RESULT_OK = -1; + + @IntDef({STANDARD, SINGLETOP, SINGLETASK}) + @Retention(RetentionPolicy.SOURCE) + @interface LaunchMode { + } + + SupportFragmentDelegate getSupportDelegate(); + + void setFragmentResult(int resultCode, Bundle bundle); + + void onFragmentResult(int requestCode, int resultCode, Bundle data); + + boolean onBackPressedSupport(); +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportActivityDelegate.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportActivityDelegate.java new file mode 100644 index 0000000..7e09ea3 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportActivityDelegate.java @@ -0,0 +1,99 @@ +package com.chwl.library.common.fragmentation; + +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import com.chwl.library.common.fragmentation.queue.Action; + +public class SupportActivityDelegate { + private final ISupportActivity mSupport; + private final FragmentActivity mActivity; + + private TransactionDelegate mTransactionDelegate; + + public SupportActivityDelegate(ISupportActivity support) { + if (!(support instanceof FragmentActivity)) + throw new RuntimeException("Must extends FragmentActivity/AppCompatActivity"); + this.mSupport = support; + this.mActivity = (FragmentActivity) support; + } + + public void onCreate() { + mTransactionDelegate = getTransactionDelegate(); + } + + public TransactionDelegate getTransactionDelegate() { + if (mTransactionDelegate == null) { + mTransactionDelegate = new TransactionDelegate(); + } + return mTransactionDelegate; + } + + /** + * 不建议复写该方法,请使用 {@link #onBackPressedSupport} 代替 + */ + public void onBackPressedPage() { + mTransactionDelegate.mActionQueue.enqueue(new Action(Action.ACTION_BACK, getSupportFragmentManager()) { + @Override + public void run() { + // 获取activeFragment:即从栈顶开始 状态为show的那个Fragment + ISupportFragment activeFragment = SupportHelper.getAddedFragment(getSupportFragmentManager()); + if (mTransactionDelegate.dispatchBackPressedEvent(activeFragment)) return; + + mSupport.onBackPressedSupport(); + } + }); + } + + /** + * 该方法回调时机为,Activity回退栈内Fragment的数量 小于等于1 时,默认finish Activity + * 请尽量复写该方法,避免复写onBackPress(),以保证SupportFragment内的onBackPressedSupport()回退事件正常执行 + */ + public void onBackPressedSupport() { + if (getSupportFragmentManager().getBackStackEntryCount() > 1) { + pop(); + } else { + ActivityCompat.finishAfterTransition(mActivity); + } + } + + //**********************************************************************************************// + + /** + * 加载根Fragment, 即Activity内的第一个Fragment 或 Fragment内的第一个子Fragment + */ + public void loadRootFragment(int containerId, ISupportFragment toFragment) { + loadRootFragment(containerId, toFragment, true); + } + + public void loadRootFragment(int containerId, ISupportFragment toFragment, boolean addToBackStack) { + mTransactionDelegate.loadRootTransaction(getSupportFragmentManager(), containerId, toFragment, addToBackStack); + } + + public void start(ISupportFragment toFragment) { + start(toFragment, ISupportFragment.STANDARD); + } + + /** + * @param launchMode Similar to Activity's LaunchMode. + */ + public void start(ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) { + mTransactionDelegate.dispatchStartTransaction(getSupportFragmentManager(), getTopFragment(), toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD); + } + + /** + * Pop the child fragment. + */ + public void pop() { + mTransactionDelegate.pop(getSupportFragmentManager()); + } + + private FragmentManager getSupportFragmentManager() { + return mActivity.getSupportFragmentManager(); + } + + private ISupportFragment getTopFragment() { + return SupportHelper.getTopFragment(getSupportFragmentManager()); + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportFragmentDelegate.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportFragmentDelegate.java new file mode 100644 index 0000000..b77fc70 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportFragmentDelegate.java @@ -0,0 +1,172 @@ +package com.chwl.library.common.fragmentation; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import com.chwl.library.common.fragmentation.internal.ResultRecord; +import com.chwl.library.common.fragmentation.internal.TransactionRecord; + +public class SupportFragmentDelegate { + int mContainerId; + + private TransactionDelegate mTransactionDelegate; + TransactionRecord mTransactionRecord; + + private final ISupportFragment mSupportF; + private final Fragment mFragment; + + public SupportFragmentDelegate(ISupportFragment support) { + if (!(support instanceof Fragment)) + throw new RuntimeException("Must extends Fragment"); + this.mSupportF = support; + this.mFragment = (Fragment) support; + } + + public void onAttach() { + FragmentActivity activity = mFragment.getActivity(); + if (activity instanceof ISupportActivity) { + ISupportActivity mSupportA = (ISupportActivity) activity; + mTransactionDelegate = mSupportA.getSupportDelegate().getTransactionDelegate(); + } else { + if (activity != null) { + throw new RuntimeException(activity.getClass().getSimpleName() + " must impl ISupportActivity!"); + } else { + throw new RuntimeException("fragment attached activity must not be null"); + } + } + } + + public void onCreate(@Nullable Bundle savedInstanceState) { + Bundle bundle = mFragment.getArguments(); + if (bundle != null) { + mContainerId = bundle.getInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER); + } + + if (savedInstanceState != null) { + savedInstanceState.setClassLoader(getClass().getClassLoader()); + mContainerId = savedInstanceState.getInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putInt(TransactionDelegate.FRAGMENTATION_ARG_CONTAINER, mContainerId); + } + + public void onViewCreated() { + View view = mFragment.getView(); + if (view != null) { + view.setClickable(true); + setBackground(view); + } + } + + public void onDestroyView() { + mTransactionDelegate.handleResultRecord(mFragment); + } + + /** + * 类似 {@link Activity#setResult(int, Intent)} + *

+ * Similar to {@link Activity#setResult(int, Intent)} + * + * @see #startForResult(ISupportFragment, int) + */ + public void setFragmentResult(int resultCode, Bundle bundle) { + Bundle args = mFragment.getArguments(); + if (args == null || !args.containsKey(TransactionDelegate.FRAGMENTATION_ARG_RESULT_RECORD)) { + return; + } + + ResultRecord resultRecord = args.getParcelable(TransactionDelegate.FRAGMENTATION_ARG_RESULT_RECORD); + if (resultRecord != null) { + resultRecord.resultCode = resultCode; + resultRecord.resultBundle = bundle; + } + } + + /** + * 类似Activity的onActivityResult(int, int, Intent) + * + * @see #startForResult(ISupportFragment, int) + */ + public void onFragmentResult(int requestCode, int resultCode, Bundle data) { + } + + /** + * Back Event + * + * @return false则继续向上传递, true则消费掉该事件 + */ + public boolean onBackPressedSupport() { + return false; + } + + //**********************************************************************************************// + + /** + * 加载根Fragment, 即Activity内的第一个Fragment 或 Fragment内的第一个子Fragment + */ + public void loadRootFragment(int containerId, ISupportFragment toFragment) { + loadRootFragment(containerId, toFragment, true); + } + + public void loadRootFragment(int containerId, ISupportFragment toFragment, boolean addToBackStack) { + mTransactionDelegate.loadRootTransaction(getChildFragmentManager(), containerId, toFragment, addToBackStack); + } + + public void start(ISupportFragment toFragment) { + start(toFragment, ISupportFragment.STANDARD); + } + + /** + * @param launchMode Similar to Activity's LaunchMode. + */ + public void start(final ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) { + mTransactionDelegate.dispatchStartTransaction(mFragment.getFragmentManager(), mSupportF, toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD); + } + + /** + * Launch an fragment for which you would like a result when it poped. + */ + public void startForResult(ISupportFragment toFragment, int requestCode) { + mTransactionDelegate.dispatchStartTransaction(mFragment.getFragmentManager(), mSupportF, toFragment, requestCode, ISupportFragment.STANDARD, TransactionDelegate.TYPE_ADD_RESULT); + } + + public void startChild(ISupportFragment toFragment) { + startChild(toFragment, ISupportFragment.STANDARD); + } + + public void startChild(final ISupportFragment toFragment, @ISupportFragment.LaunchMode int launchMode) { + mTransactionDelegate.dispatchStartTransaction(getChildFragmentManager(), getTopFragment(), toFragment, 0, launchMode, TransactionDelegate.TYPE_ADD); + } + + public void pop() { + mTransactionDelegate.pop(mFragment.getFragmentManager()); + } + + private FragmentManager getChildFragmentManager() { + return mFragment.getChildFragmentManager(); + } + + private ISupportFragment getTopFragment() { + return SupportHelper.getTopFragment(getChildFragmentManager()); + } + + public void setBackground(View view) { + if ((mFragment.getTag() != null && mFragment.getTag().startsWith("android:switcher:")) || + view.getBackground() != null) { + return; + } + + view.setBackgroundColor(Color.TRANSPARENT); + } + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportHelper.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportHelper.java new file mode 100644 index 0000000..a3efae8 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/SupportHelper.java @@ -0,0 +1,209 @@ +package com.chwl.library.common.fragmentation; + +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by YoKey on 17/6/13. + */ + +public class SupportHelper { + private static final long SHOW_SPACE = 200L; + + private SupportHelper() { + } + + /** + * 显示软键盘 + */ + public static void showSoftInput(final View view) { + if (view == null || view.getContext() == null) return; + final InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + view.requestFocus(); + view.postDelayed(new Runnable() { + @Override + public void run() { + imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); + } + }, SHOW_SPACE); + } + + /** + * 隐藏软键盘 + */ + public static void hideSoftInput(View view) { + if (view == null || view.getContext() == null) return; + InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * 获得栈顶SupportFragment + */ + public static ISupportFragment getTopFragment(FragmentManager fragmentManager) { + return getTopFragment(fragmentManager, 0); + } + + public static ISupportFragment getTopFragment(FragmentManager fragmentManager, int containerId) { + List fragmentList = fragmentManager.getFragments(); + + for (int i = fragmentList.size() - 1; i >= 0; i--) { + Fragment fragment = fragmentList.get(i); + if (fragment instanceof ISupportFragment) { + ISupportFragment iFragment = (ISupportFragment) fragment; + if (containerId == 0) return iFragment; + + if (containerId == iFragment.getSupportDelegate().mContainerId) { + return iFragment; + } + } + } + return null; + } + + /** + * Same as fragmentManager.findFragmentByTag(fragmentClass.getName()); + * find Fragment from FragmentStack + */ + public static T findFragment(FragmentManager fragmentManager, Class fragmentClass) { + return findAddedFragment(fragmentClass, null, fragmentManager); + } + + /** + * Same as fragmentManager.findFragmentByTag(fragmentTag); + *

+ * find Fragment from FragmentStack + */ + public static T findFragment(FragmentManager fragmentManager, String fragmentTag) { + return findAddedFragment(null, fragmentTag, fragmentManager); + } + + /** + * 从栈顶开始,寻找FragmentManager以及其所有子栈, 直到找到状态为show & userVisible的Fragment + */ + public static ISupportFragment getAddedFragment(FragmentManager fragmentManager) { + return getAddedFragment(fragmentManager, null); + } + + static T findAddedFragment(Class fragmentClass, String toFragmentTag, FragmentManager fragmentManager) { + Fragment fragment = null; + + if (toFragmentTag == null) { + List fragmentList = fragmentManager.getFragments(); + + int sizeChildFrgList = fragmentList.size(); + + for (int i = sizeChildFrgList - 1; i >= 0; i--) { + Fragment brotherFragment = fragmentList.get(i); + if (brotherFragment instanceof ISupportFragment && brotherFragment.getClass().getName().equals(fragmentClass.getName())) { + fragment = brotherFragment; + break; + } + } + } else { + fragment = fragmentManager.findFragmentByTag(toFragmentTag); + if (fragment == null) return null; + } + return (T) fragment; + } + + private static ISupportFragment getAddedFragment(FragmentManager fragmentManager, ISupportFragment parentFragment) { + List fragmentList = fragmentManager.getFragments(); + if (fragmentList.size() == 0) { + return parentFragment; + } + for (int i = fragmentList.size() - 1; i >= 0; i--) { + Fragment fragment = fragmentList.get(i); + if (fragment instanceof ISupportFragment) { + if (fragment.isResumed() && !fragment.isHidden() && fragment.getUserVisibleHint()) { + return getAddedFragment(fragment.getChildFragmentManager(), (ISupportFragment) fragment); + } + } + } + return parentFragment; + } + + /** + * Get the topFragment from BackStack + */ + public static ISupportFragment getBackStackTopFragment(FragmentManager fragmentManager) { + return getBackStackTopFragment(fragmentManager, 0); + } + + /** + * Get the topFragment from BackStack + */ + public static ISupportFragment getBackStackTopFragment(FragmentManager fragmentManager, int containerId) { + int count = fragmentManager.getBackStackEntryCount(); + + for (int i = count - 1; i >= 0; i--) { + FragmentManager.BackStackEntry entry = fragmentManager.getBackStackEntryAt(i); + Fragment fragment = fragmentManager.findFragmentByTag(entry.getName()); + if (fragment instanceof ISupportFragment) { + ISupportFragment supportFragment = (ISupportFragment) fragment; + if (containerId == 0) return supportFragment; + + if (containerId == supportFragment.getSupportDelegate().mContainerId) { + return supportFragment; + } + } + } + return null; + } + + static T findBackStackFragment(Class fragmentClass, String toFragmentTag, FragmentManager fragmentManager) { + int count = fragmentManager.getBackStackEntryCount(); + + if (toFragmentTag == null) { + toFragmentTag = fragmentClass.getName(); + } + + for (int i = count - 1; i >= 0; i--) { + FragmentManager.BackStackEntry entry = fragmentManager.getBackStackEntryAt(i); + + if (toFragmentTag.equals(entry.getName())) { + Fragment fragment = fragmentManager.findFragmentByTag(entry.getName()); + if (fragment instanceof ISupportFragment) { + return (T) fragment; + } + } + } + return null; + } + + static List getWillPopFragments(FragmentManager fm, String targetTag) { + Fragment target = fm.findFragmentByTag(targetTag); + List willPopFragments = new ArrayList<>(); + + List fragmentList = fm.getFragments(); + + int size = fragmentList.size(); + + int startIndex = -1; + for (int i = size - 1; i >= 0; i--) { + if (target == fragmentList.get(i)) { + if (i + 1 < size) { + startIndex = i + 1; + } + break; + } + } + + if (startIndex == -1) return willPopFragments; + + for (int i = size - 1; i >= startIndex; i--) { + Fragment fragment = fragmentList.get(i); + if (fragment != null && fragment.getView() != null) { + willPopFragments.add(fragment); + } + } + return willPopFragments; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/TransactionDelegate.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/TransactionDelegate.java new file mode 100644 index 0000000..1bfe2ae --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/TransactionDelegate.java @@ -0,0 +1,305 @@ +package com.chwl.library.common.fragmentation; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.chwl.library.common.fragmentation.internal.ResultRecord; +import com.chwl.library.common.fragmentation.internal.TransactionRecord; +import com.chwl.library.common.fragmentation.queue.Action; +import com.chwl.library.common.fragmentation.queue.ActionQueue; + +import java.util.List; + + +/** + * Controller + * Created by YoKeyword on 16/1/22. + */ +class TransactionDelegate { + private static final String TAG = "Fragmentation"; + + static final String FRAGMENTATION_ARG_RESULT_RECORD = "fragment_arg_result_record"; + static final String FRAGMENTATION_ARG_CONTAINER = "fragmentation_arg_container"; + static final String FRAGMENTATION_ARG_REPLACE = "fragmentation_arg_replace"; + + private static final String FRAGMENTATION_STATE_SAVE_RESULT = "fragmentation_state_save_result"; + + static final int TYPE_ADD = 0; + static final int TYPE_ADD_RESULT = 1; + static final int TYPE_ADD_WITHOUT_HIDE = 2; + static final int TYPE_ADD_RESULT_WITHOUT_HIDE = 3; + static final int TYPE_REPLACE = 10; + static final int TYPE_REPLACE_DONT_BACK = 11; + + ActionQueue mActionQueue; + + TransactionDelegate() { + Handler mHandler = new Handler(Looper.getMainLooper()); + mActionQueue = new ActionQueue(mHandler); + } + + void loadRootTransaction(final FragmentManager fm, final int containerId, final ISupportFragment to, final boolean addToBackStack) { + enqueue(fm, new Action(Action.ACTION_LOAD, fm) { + @Override + public void run() { + bindContainerId(containerId, to); + + String toFragmentTag = to.getClass().getName(); + TransactionRecord transactionRecord = to.getSupportDelegate().mTransactionRecord; + if (transactionRecord != null) { + if (transactionRecord.tag != null) { + toFragmentTag = transactionRecord.tag; + } + } + + start(fm, null, to, toFragmentTag, !addToBackStack, TYPE_REPLACE); + } + }); + } + + + private void start(FragmentManager fm, final ISupportFragment from, ISupportFragment to, String toFragmentTag, boolean dontAddToBackStack, int type) { + FragmentTransaction ft = fm.beginTransaction(); + boolean addMode = (type == TYPE_ADD || type == TYPE_ADD_RESULT || type == TYPE_ADD_WITHOUT_HIDE || type == TYPE_ADD_RESULT_WITHOUT_HIDE); + Fragment fromF = (Fragment) from; + Fragment toF = (Fragment) to; + Bundle args = getArguments(toF); + args.putBoolean(FRAGMENTATION_ARG_REPLACE, !addMode); + + if (from == null) { + ft.replace(args.getInt(FRAGMENTATION_ARG_CONTAINER), toF, toFragmentTag); + if (!addMode) { + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + } + } else { + if (addMode) { + ft.add(from.getSupportDelegate().mContainerId, toF, toFragmentTag); + if (type != TYPE_ADD_WITHOUT_HIDE && type != TYPE_ADD_RESULT_WITHOUT_HIDE) { + ft.hide(fromF); + } + } else { + ft.replace(from.getSupportDelegate().mContainerId, toF, toFragmentTag); + } + } + + if (!dontAddToBackStack && type != TYPE_REPLACE_DONT_BACK) { + ft.addToBackStack(toFragmentTag); + } + supportCommit(ft); + } + + /** + * Pop + */ + void pop(final FragmentManager fm) { + enqueue(fm, new Action(Action.ACTION_POP, fm) { + @Override + public void run() { + removeTopFragment(fm); + fm.popBackStackImmediate(); + } + }); + } + + private void removeTopFragment(FragmentManager fm) { + try { // Safe popBackStack() + ISupportFragment top = SupportHelper.getBackStackTopFragment(fm); + if (top != null) { + fm.beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) + .remove((Fragment) top) + .commitAllowingStateLoss(); + } + } catch (Exception ignored) { + + } + } + + /** + * Dispatch the start transaction. + */ + void dispatchStartTransaction(final FragmentManager fm, final ISupportFragment from, final ISupportFragment to, final int requestCode, final int launchMode, final int type) { + enqueue(fm, new Action(launchMode == ISupportFragment.SINGLETASK ? Action.ACTION_POP_MOCK : Action.ACTION_NORMAL, fm) { + @Override + public void run() { + doDispatchStartTransaction(fm, from, to, requestCode, launchMode, type); + } + }); + } + + private void doDispatchStartTransaction(FragmentManager fm, ISupportFragment from, ISupportFragment to, int requestCode, int launchMode, int type) { + if (to == null) { + throw new NullPointerException("toFragment == null"); + } + + if ((type == TYPE_ADD_RESULT || type == TYPE_ADD_RESULT_WITHOUT_HIDE) && from != null) { + if (!((Fragment) from).isAdded()) { + Log.w(TAG, ((Fragment) from).getClass().getSimpleName() + " has not been attached yet! startForResult() converted to start()"); + } else { + saveRequestCode(fm, (Fragment) from, (Fragment) to, requestCode); + } + } + + from = getTopFragmentForStart(from, fm); + + int containerId = getArguments((Fragment) to).getInt(FRAGMENTATION_ARG_CONTAINER, 0); + if (from == null && containerId == 0) { + Log.e(TAG, "There is no Fragment in the FragmentManager, maybe you need to call loadRootFragment()!"); + return; + } + + if (from != null && containerId == 0) { + bindContainerId(from.getSupportDelegate().mContainerId, to); + } + + // process ExtraTransaction + String toFragmentTag = to.getClass().getName(); + boolean dontAddToBackStack = false; + TransactionRecord transactionRecord = to.getSupportDelegate().mTransactionRecord; + if (transactionRecord != null) { + if (transactionRecord.tag != null) { + toFragmentTag = transactionRecord.tag; + } + dontAddToBackStack = transactionRecord.dontAddToBackStack; + } + + if (handleLaunchMode(fm, from, to, toFragmentTag, launchMode)) return; + + start(fm, from, to, toFragmentTag, dontAddToBackStack, type); + } + + private void doPopTo(final String targetFragmentTag, FragmentManager fm) { + Fragment targetFragment = fm.findFragmentByTag(targetFragmentTag); + + if (targetFragment == null) { + Log.e(TAG, "Pop failure! Can't find FragmentTag:" + targetFragmentTag + " in the FragmentManager's Stack."); + return; + } + + List willPopFragments = SupportHelper.getWillPopFragments(fm, targetFragmentTag); + if (willPopFragments.size() <= 0) return; + safePopTo(targetFragmentTag, fm, willPopFragments); + } + + private void safePopTo(String fragmentTag, final FragmentManager fm, List willPopFragments) { + // 批量删除fragment ,static final int OP_REMOVE = 3; + FragmentTransaction transaction = fm.beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); + for (Fragment fragment : willPopFragments) { + transaction.remove(fragment); + } + transaction.commitAllowingStateLoss(); + + // 弹栈到指定fragment,从数据上来看,和上面的效果完全一样,把栈中所有的backStackRecord 包含多个记录,每个记录包含多个操作 , + // 在每个记录中,对操作索引 按照从大到小的顺序,逐个进行反操作。 + // 除了第一个记录,其余每个记录都有两个操作,一个是添加OP_ADD = 1;(反操作是remove) 一个是OP_HIDE = 4;(反操作是show)(这是在start中设定的) + // 之所以有上面的批量删除,在执行动画的时候,发现f.mView == null 就不去执行show动画。 + fm.popBackStackImmediate(fragmentTag, 0); + } + + private ISupportFragment getTopFragmentForStart(ISupportFragment from, FragmentManager fm) { + ISupportFragment top; + if (from == null) { + top = SupportHelper.getTopFragment(fm); + } else { + if (from.getSupportDelegate().mContainerId == 0) { + Fragment fromF = (Fragment) from; + if (fromF.getTag() != null && !fromF.getTag().startsWith("android:switcher:")) { + throw new IllegalStateException("Can't find container, please call loadRootFragment() first!"); + } + } + top = SupportHelper.getTopFragment(fm, from.getSupportDelegate().mContainerId); + } + return top; + } + + /** + * Dispatch the pop-event. Priority of the top of the stack of Fragment + */ + boolean dispatchBackPressedEvent(ISupportFragment activeFragment) { + if (activeFragment != null) { + boolean result = activeFragment.onBackPressedSupport(); + if (result) { + return true; + } + + Fragment parentFragment = ((Fragment) activeFragment).getParentFragment(); + return dispatchBackPressedEvent((ISupportFragment) parentFragment); + } + return false; + } + + void handleResultRecord(Fragment from) { + try { + Bundle args = from.getArguments(); + if (args == null) return; + final ResultRecord resultRecord = args.getParcelable(FRAGMENTATION_ARG_RESULT_RECORD); + if (resultRecord == null) return; + + ISupportFragment targetFragment = (ISupportFragment) from.getFragmentManager().getFragment(from.getArguments(), FRAGMENTATION_STATE_SAVE_RESULT); + targetFragment.onFragmentResult(resultRecord.requestCode, resultRecord.resultCode, resultRecord.resultBundle); + } catch (IllegalStateException ignored) { + // Fragment no longer exists + } + } + + private void enqueue(FragmentManager fm, Action action) { + if (fm == null) { + Log.w(TAG, "FragmentManager is null, skip the action!"); + return; + } + mActionQueue.enqueue(action); + } + + private void bindContainerId(int containerId, ISupportFragment to) { + Bundle args = getArguments((Fragment) to); + args.putInt(FRAGMENTATION_ARG_CONTAINER, containerId); + } + + private Bundle getArguments(Fragment fragment) { + Bundle bundle = fragment.getArguments(); + if (bundle == null) { + bundle = new Bundle(); + fragment.setArguments(bundle); + } + return bundle; + } + + private void supportCommit(FragmentTransaction transaction) { + transaction.commitAllowingStateLoss(); + } + + private boolean handleLaunchMode(FragmentManager fm, ISupportFragment topFragment, final ISupportFragment to, String toFragmentTag, int launchMode) { + if (topFragment == null) return false; + final ISupportFragment stackToFragment = SupportHelper.findBackStackFragment(to.getClass(), toFragmentTag, fm); + if (stackToFragment == null) return false; + + if (launchMode == ISupportFragment.SINGLETOP) { + if (to == topFragment || to.getClass().getName().equals(topFragment.getClass().getName())) { + return true; + } + } else if (launchMode == ISupportFragment.SINGLETASK) { + doPopTo(toFragmentTag, fm); + return true; + } + return false; + } + + /** + * save requestCode + */ + private void saveRequestCode(FragmentManager fm, Fragment from, Fragment to, int requestCode) { + Bundle bundle = getArguments(to); + ResultRecord resultRecord = new ResultRecord(); + resultRecord.requestCode = requestCode; + bundle.putParcelable(FRAGMENTATION_ARG_RESULT_RECORD, resultRecord); + fm.putFragment(bundle, FRAGMENTATION_STATE_SAVE_RESULT, from); + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/ResultRecord.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/ResultRecord.java new file mode 100644 index 0000000..002a0f3 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/ResultRecord.java @@ -0,0 +1,48 @@ +package com.chwl.library.common.fragmentation.internal; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @Hide Result 记录 + * Created by YoKeyword on 16/6/2. + */ +public final class ResultRecord implements Parcelable { + public int requestCode; + public int resultCode = 0; + public Bundle resultBundle; + + public ResultRecord() { + } + + protected ResultRecord(Parcel in) { + requestCode = in.readInt(); + resultCode = in.readInt(); + resultBundle = in.readBundle(getClass().getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ResultRecord createFromParcel(Parcel in) { + return new ResultRecord(in); + } + + @Override + public ResultRecord[] newArray(int size) { + return new ResultRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requestCode); + dest.writeInt(resultCode); + dest.writeBundle(resultBundle); + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/TransactionRecord.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/TransactionRecord.java new file mode 100644 index 0000000..1e942bf --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/internal/TransactionRecord.java @@ -0,0 +1,9 @@ +package com.chwl.library.common.fragmentation.internal; + +/** + * @hide Created by YoKey on 16/11/25. + */ +public final class TransactionRecord { + public String tag; + public boolean dontAddToBackStack = false; +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/Action.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/Action.java new file mode 100644 index 0000000..88fbdb1 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/Action.java @@ -0,0 +1,35 @@ +package com.chwl.library.common.fragmentation.queue; + +import androidx.fragment.app.FragmentManager; + +/** + * Created by YoKey on 17/12/28. + */ +public abstract class Action { + public static final long DEFAULT_POP_TIME = 300L; + + public static final int ACTION_NORMAL = 0; + public static final int ACTION_POP = 1; + public static final int ACTION_POP_MOCK = 2; + public static final int ACTION_BACK = 3; + public static final int ACTION_LOAD = 4; + + public FragmentManager fragmentManager; + public int action = ACTION_NORMAL; + public long duration = 0; + + public Action(int action) { + this.action = action; + } + + public Action(int action, FragmentManager fragmentManager) { + this(action); + this.fragmentManager = fragmentManager; + } + + public Action(FragmentManager fragmentManager) { + this.fragmentManager = fragmentManager; + } + + public abstract void run(); +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/ActionQueue.java b/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/ActionQueue.java new file mode 100644 index 0000000..d984a4b --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/queue/ActionQueue.java @@ -0,0 +1,81 @@ +package com.chwl.library.common.fragmentation.queue; + +import android.os.Handler; +import android.os.Looper; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * The queue of perform action. + *

+ * Created by YoKey on 17/12/29. + */ +public class ActionQueue { + private Queue mQueue = new LinkedList<>(); + private Handler mMainHandler; + + public ActionQueue(Handler mainHandler) { + this.mMainHandler = mainHandler; + } + + public void enqueue(final Action action) { + if (isThrottleBACK(action)) return; + + if (action.action == Action.ACTION_LOAD && mQueue.isEmpty() + && Thread.currentThread() == Looper.getMainLooper().getThread()) { + action.run(); + return; + } + + mMainHandler.post(new Runnable() { + @Override + public void run() { + enqueueAction(action); + } + }); + } + + private void enqueueAction(Action action) { + mQueue.add(action); + //第一次进来的时候,执行完上局,队列只有一个,一旦进入handleAction,就会一直执行,直到队列为空 + if (mQueue.size() == 1) { + handleAction(); + } + } + + private void handleAction() { + if (mQueue.isEmpty()) return; + + Action action = mQueue.peek(); + if (action == null || action.fragmentManager.isStateSaved()) { + mQueue.clear(); + return; + } + action.run(); + + executeNextAction(action); + } + + private void executeNextAction(Action action) { + if (action.action == Action.ACTION_POP) { + action.duration = Action.DEFAULT_POP_TIME; + } + + mMainHandler.postDelayed(new Runnable() { + @Override + public void run() { + mQueue.poll(); + handleAction(); + } + }, action.duration); + } + + private boolean isThrottleBACK(Action action) { + if (action.action == Action.ACTION_BACK) { + Action head = mQueue.peek(); + return head != null && head.action == Action.ACTION_POP; + } + return false; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/IWindowCallbackProxy.kt b/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/IWindowCallbackProxy.kt new file mode 100644 index 0000000..2383da9 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/IWindowCallbackProxy.kt @@ -0,0 +1,14 @@ +package com.chwl.library.common.fragmentation.windowcallback + +import android.view.Window + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: + */ +interface IWindowCallbackProxy { + fun createCallBack( + callback: Window.Callback?, isDialog: Boolean, tag: String? + ): Window.Callback? +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/WindowCallbackProxyUtil.kt b/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/WindowCallbackProxyUtil.kt new file mode 100644 index 0000000..d5d226f --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/fragmentation/windowcallback/WindowCallbackProxyUtil.kt @@ -0,0 +1,25 @@ +package com.chwl.library.common.fragmentation.windowcallback + +import android.view.Window + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: WindowCallback代理类工具 + */ +object WindowCallbackProxyUtil { + var mCallbackProxy: IWindowCallbackProxy? = null + + @JvmStatic + fun setWindowCallbackProxy(proxy: IWindowCallbackProxy?) { + mCallbackProxy = proxy + } + + @JvmStatic + @JvmOverloads + fun createWindowCallBack( + callback: Window.Callback?, isDialog: Boolean = false, tag: String? = null + ): Window.Callback? { + return mCallbackProxy?.createCallBack(callback, isDialog, tag) + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/glide/AnimEffectUtil.kt b/library/src/module_common/java/com/chwl/library/common/glide/AnimEffectUtil.kt new file mode 100644 index 0000000..d4ac20d --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/glide/AnimEffectUtil.kt @@ -0,0 +1,434 @@ +//package com.kelly.dawi.util +// +//import android.graphics.Bitmap +//import android.graphics.Paint +//import android.os.Looper +//import android.text.TextPaint +//import android.view.View +//import android.widget.ImageView +//import androidx.annotation.DrawableRes +//import androidx.lifecycle.findViewTreeLifecycleOwner +//import androidx.lifecycle.lifecycleScope +//import com.bumptech.glide.Glide +//import com.bumptech.glide.load.DataSource +//import com.bumptech.glide.load.engine.GlideException +//import com.bumptech.glide.request.FutureTarget +//import com.bumptech.glide.request.RequestFutureTarget +//import com.bumptech.glide.request.RequestListener +//import com.bumptech.glide.request.RequestOptions +//import com.bumptech.glide.request.target.Target +//import com.kelly.dawi.dp +//import com.kelly.dawi.getOrNull +//import com.kelly.dawi.simpleImpl.SimpleSvgaCallback +//import com.kelly.dawi.suspendGetOrNull +//import com.opensource.svgaplayer.SVGAImageView +//import com.opensource.svgaplayer.glideplugin.asSVGADrawable +//import com.tencent.qgame.animplayer.AnimView +//import com.tencent.qgame.animplayer.inter.IFetchResource +//import com.tencent.qgame.animplayer.mix.Resource +//import com.tencent.qgame.animplayer.util.ScaleType +//import com.kelly.dawi.simpleImpl.SimpleVapCallback +//import kotlinx.coroutines.* +//import java.io.File +//import java.lang.RuntimeException +//import java.util.concurrent.CancellationException +// +///** +// * @Author Vance +// * @Date 2022/6/17 0017 13:44 +// */ +// +//object AnimEffectUtil { +// +// @JvmStatic +// fun newBuilder(): ExtBuilder { +// return ExtBuilder() +// } +// +// @JvmStatic +// fun load(url: String?): ExtBuilder { +// return ExtBuilder().load(url) +// } +// +// class ExtBuilder : IFetchResource, View.OnAttachStateChangeListener { +// private val defTextPaint = TextPaint().apply { textAlign = Paint.Align.CENTER } +// +// private var job: Job? = null +// private var extMap = mutableMapOf() +// private var glideRequests = mutableListOf?>() +// +// private var tempExtMap = mutableMapOf() +// private var listener: Listener? = null +// private var repeatCount: Int? = null +// private var clearsAfterDetached: Boolean? = null +// private var scaleType: ImageView.ScaleType? = null +// +// private var url: String? = null +// +// private var skipMemoryCache = true +// +// private var scope:CoroutineScope? = null +// +// fun load(url: String?): ExtBuilder { +// this.url = url +// return this +// } +// +// fun setCoroutineScope(scope:CoroutineScope): ExtBuilder { +// this.scope = scope +// return this +// } +// +// fun skipMemoryCache(skipMemoryCache: Boolean): ExtBuilder { +// this.skipMemoryCache = skipMemoryCache +// return this +// } +// +// fun scaleType(scaleType: ImageView.ScaleType): ExtBuilder { +// this.scaleType = scaleType +// return this +// } +// +// fun clearsAfterDetached(clearsAfterDetached: Boolean): ExtBuilder { +// this.clearsAfterDetached = clearsAfterDetached +// return this +// } +// +// fun listener(listener: Listener?): ExtBuilder { +// this.listener = listener +// return this +// } +// +// fun repeatCount(repeatCount: Int): ExtBuilder { +// this.repeatCount = repeatCount +// return this +// } +// +// /** +// * textPain是SVGA用的 +// */ +// @JvmOverloads +// fun putText(key: String, text: String?, textPaint: TextPaint = defTextPaint): ExtBuilder { +// tempExtMap[key] = Ext(text, textPaint) +// return this +// } +// +// @JvmOverloads +// fun putImg( +// key: String, +// imgUrl: Any?, +// widthDp: Float = 0f, +// heightDp: Float = 0f, +// isCircle: Boolean = true, +// @DrawableRes default: Int = 0 +// ): ExtBuilder { +// tempExtMap[key] = Ext( +// imgGetter = { +// val option = RequestOptions() +// if (isCircle) { +// option.circleCrop() +// } +// if (widthDp > 0 && heightDp > 0) { +// option.override(widthDp.dp, heightDp.dp) +// } +// +// val futureTarget = RequestFutureTarget(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) +// glideRequests.add(futureTarget) +// var bitmap: Bitmap? = null +// try { +// bitmap = Glide.with(MyUtils.application) +// .asBitmap() +// .load(imgUrl) +// .apply(option) +// .addListener(futureTarget) +// .into(futureTarget) +// .get() +// }catch (e: InterruptedException){ +// glideRequests.remove(futureTarget) +// return@Ext null //取消掉的直接返回不去加载默认图了 +// }catch (e: CancellationException){ +// glideRequests.remove(futureTarget) +// return@Ext null //取消掉的直接返回不去加载默认图了 +// }catch (e: Exception){ +// glideRequests.remove(futureTarget) +// } +// +// if (bitmap == null && default != 0) { +// val defFutureTarget = RequestFutureTarget(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) +// glideRequests.add(defFutureTarget) +// bitmap = Glide.with(MyUtils.application) +// .asBitmap() +// .load(default) +// .apply(option) +// .addListener(defFutureTarget) +// .into(defFutureTarget) +// .getOrNull() +// } +// bitmap +// }) +// return this +// } +// +// fun into(svgaImageView: SVGAImageView){ +// setup(svgaImageView) +// val animUrl = url +// if(animUrl.isNullOrBlank()){ +// listener?.onAnimError(RuntimeException("url is empty")) +// return +// } +// +// var svgaUrl = animUrl +// if(!animUrl.startsWith("http")){//没有http前缀就当成asset文件加载 +// svgaUrl = "file:///android_asset/$animUrl" +// } +// +// if(scope == null){ +// scope = svgaImageView.findViewTreeLifecycleOwner()?.lifecycleScope +// } +// +// job = scope?.launch(Dispatchers.IO) { +// val svgaJob = async { +// Glide.with(svgaImageView.context) +// .asSVGADrawable() +// .load(svgaUrl.trim()) +// .skipMemoryCache(skipMemoryCache) +// .submit() +// .suspendGetOrNull() +// } +// +// val bitmapJobMap = mutableMapOf>() +// val textMap = mutableMapOf() +// val paramsMap = mutableMapOf() +// try { +// paramsMap.putAll(extMap) +// paramsMap.forEach { +// ensureActive() +// if (it.value.isImg()) { +// val job = async { it.value.getBitMap() } +// bitmapJobMap[it.key] = job +// } else { +// textMap[it.key] = it.value +// } +// } +// } catch (_: Exception) { } +// +// val drawable = svgaJob.await() +// if (drawable == null) { +// bitmapJobMap.forEach { +// it.value.cancelAndJoin() +// } +// val imgRequests = glideRequests +// imgRequests.forEach { +// it?.cancel(true) +// } +// withContext(Dispatchers.Main) { +// onAnimError("svga loading error ") +// } +// } else { +// bitmapJobMap.forEach { entry -> +// ensureActive() +// entry.value.await()?.let { +// drawable.dynamicItem.setDynamicImage(it, entry.key) +// } +// } +// textMap.forEach { entry -> +// entry.value.text?.let { +// drawable.dynamicItem.setDynamicText(it, entry.value.textPaint, entry.key) +// } +// } +// +// withContext(Dispatchers.Main) { +// onAnimStart() +// svgaImageView.setImageDrawable(drawable) +// svgaImageView.startAnimation() +// extMap = mutableMapOf() +// glideRequests = mutableListOf() +// } +// } +// } +// } +// +// fun into(animView: AnimView){ +// setup(animView) +// val mp4Url = url +// if(mp4Url.isNullOrBlank()){ +// listener?.onAnimError(RuntimeException("url is empty")) +// return +// } +// +// animView.setFetchResource(this) +// +// if(!mp4Url.startsWith("http")){//没有http前缀就当成asset文件加载 +// onAnimStart() +// animView.startPlay(animView.context.assets, mp4Url) +// return +// } +// +// if(scope == null){ +// scope = animView.findViewTreeLifecycleOwner()?.lifecycleScope +// } +// job = scope?.launch(Dispatchers.IO) { +// Glide.with(animView.context) +// .asFile() +// .skipMemoryCache(skipMemoryCache) +// .load(mp4Url) +// .addListener(object : RequestListener { +// override fun onLoadFailed(e: GlideException?, model: Any?, target: Target, isFirstResource: Boolean): Boolean { +// e?.logRootCauses("vap_loading_error") +// animView.post { +// onAnimError("vap loading error msg:${e?.message ?: "empty"}") +// } +// return true +// } +// +// override fun onResourceReady(resource: File, model: Any, target: Target?, dataSource: DataSource, isFirstResource: Boolean): Boolean { +// return false +// } +// }) +// .submit() +// .suspendGetOrNull() +// ?.let { +// withContext(Dispatchers.Main){ +// onAnimStart() +// animView.startPlay(it) +// } +// } +// } +// } +// +// private fun setup(view: View){ +// if(Looper.myLooper() != Looper.getMainLooper()){ +// throw Exception("must run on MAIN Thread current Thread ${Thread.currentThread().name}") +// } +// cancelJob() +// extMap = mutableMapOf() +// extMap.putAll(tempExtMap) +// view.addOnAttachStateChangeListener(this) +// +// if(view is SVGAImageView){ +// if(!skipMemoryCache){ +// view.clearsAfterDetached = false +// } else { +// clearsAfterDetached?.let { view.clearsAfterDetached = it } +// } +// repeatCount?.let { view.loops = if(it <= 0) Int.MAX_VALUE else it } +// scaleType?.let { view.scaleType = it } +// view.callback = object: SimpleSvgaCallback(){ +// override fun onStep(frame: Int, percentage: Double) { +// if (percentage == 1.0) { +// view.post { onAnimComplete() } +// } +// } +// } +// }else if (view is AnimView){ +// repeatCount?.let { view.setLoop(if(it <= 0) Int.MAX_VALUE else it) } +// scaleType?.let { +// when (it) { +// ImageView.ScaleType.FIT_XY -> { +// view.setScaleType(ScaleType.FIT_XY) +// } +// ImageView.ScaleType.CENTER_CROP -> { +// view.setScaleType(ScaleType.CENTER_CROP) +// } +// else -> { +// view.setScaleType(ScaleType.FIT_CENTER) +// } +// } +// } +// view.setAnimListener(object : SimpleVapCallback(){ +// override fun onFailed(errorType: Int, errorMsg: String?) { +// view.post { onAnimError("vap errorType:${errorMsg} errorMsg:${errorMsg ?: "empty"}") } +// } +// +// override fun onVideoComplete() { +// view.post { onAnimComplete() } +// } +// }) +// } +// } +// +// fun clear(){ +// tempExtMap = mutableMapOf() +// extMap = mutableMapOf() +// cancelJob() +// } +// +// private fun cancelJob(){ +// try { +// job?.cancel() +// val imgRequests = glideRequests +// glideRequests = mutableListOf() +// imgRequests.forEach { +// it?.cancel(true) +// } +// } catch (_: Exception) { } +// } +// +// private fun onAnimStart() { +// listener?.onAnimStart() +// } +// +// private fun onAnimComplete() { +// extMap = mutableMapOf() +// glideRequests = mutableListOf() +// listener?.onAnimComplete() +// } +// +// private fun onAnimError(msg: String) { +// extMap = mutableMapOf() +// cancelJob() +// listener?.onAnimError(Exception(msg)) +// } +// +// override fun fetchImage(resource: Resource, result: (Bitmap?) -> Unit) { +// val ext = extMap[resource.tag] +// result(ext?.getBitMap()) +// } +// +// override fun fetchText(resource: Resource, result: (String?) -> Unit) { +// val ext = extMap[resource.tag] +// result(ext?.text) +// } +// +// override fun releaseResource(resources: List) { +// } +// +// private inner class Ext( +// val text: String? = null, +// val textPaint: TextPaint = defTextPaint, +// val imgGetter: (() -> Bitmap?)? = null) { +// fun getBitMap(): Bitmap? { +// return try { +// val bitmap = imgGetter?.invoke() +// bitmap +// } catch (t: Throwable) { +// null +// } +// } +// +// fun isImg(): Boolean { +// return text.isNullOrBlank() +// } +// +// fun isText(): Boolean { +// return !text.isNullOrBlank() +// } +// } +// +// override fun onViewAttachedToWindow(v: View) { +// } +// +// override fun onViewDetachedFromWindow(v: View) { +// v.removeOnAttachStateChangeListener(this) +// cancelJob() +// } +// } +// +// interface Listener { +// fun onAnimStart() {} +// fun onAnimComplete() +// fun onAnimError(e: Exception?) { +// onAnimComplete() +// } +// } +//} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/glide/GlideUtils.kt b/library/src/module_common/java/com/chwl/library/common/glide/GlideUtils.kt new file mode 100644 index 0000000..5fe896c --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/glide/GlideUtils.kt @@ -0,0 +1,1592 @@ +package com.chwl.library.common.glide + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.ContextWrapper +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.TextUtils +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.annotation.WorkerThread +import com.bumptech.glide.GenericTransitionOptions +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.RequestManager +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.DrawableImageViewTarget +import com.bumptech.glide.request.target.Target +import com.chwl.library.common.transform.AssignScaleTransformation +import com.chwl.library.common.transform.ComplexTransformation +import com.chwl.library.common.util.ActivityHelper +import com.chwl.library.common.util.LibLogger +import com.chwl.library.common.util.Utils +import jp.wasabeef.glide.transformations.RoundedCornersTransformation +import java.io.File + +/** + * author: create by wushaocheng on 2022/12/29 + * 图片加载工具类 + */ +class GlideUtils { + + /** + * 加载字节图片 + * @param bytes 图片字节 + */ + fun loadBytes( + bytes: ByteArray, + imageView: ImageView?, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadBytes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .asBitmap() + .load(bytes) + .transform(*transformation) + .into(imageView) + } + } + + /** + * 预加载图片到缓存中,无需设置到目标空间 + */ + fun loadNoInto(context: Context, url: String?) { + getGlideConfig(context)?.apply { + this + .load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .preload() + } + } + + /** + * 》》》》》》》》需在子线程中调用《《《《《《《《 + * 加载图片并返回bitmap图片 + */ + @WorkerThread + fun loadGetBitmap(context: Context, url: String?): Bitmap? { + if (url.isNullOrEmpty()) return null + val glideConfig = getGlideConfig(context) ?: return null + return glideConfig.asBitmap().load(url).submit().get() + } + + /** + * 》》》》》》》》需在子线程中调用《《《《《《《《 + * 加载图片并返回bitmap图片 + * @param width 图片宽 + * @param height 图片高 + */ + @WorkerThread + fun loadGetBitmap( + context: Context, + url: String?, + isCircle: Boolean? = false, + width: Int, + height: Int + ): Bitmap? { + if (url.isNullOrEmpty()) return null + val glideConfig = getGlideConfig(context) ?: return null + val load = glideConfig.asBitmap().load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + if (isCircle == true) { + load.circleCrop() + } + return load.submit(width, height).get() + } + + /** + * 》》》》》》》》需在子线程中调用《《《《《《《《 + * 获取图片的file文件,并指定大小 + * @param width 图片宽 + * @param height 图片高 + */ + @WorkerThread + fun loadGetFile(context: Context, url: String?, width: Int, height: Int): File? { + val glideConfig = getGlideConfig(context) ?: return null + return glideConfig.asFile().load(url).submit(width, height).get() + } + + /** + * 》》》》》》》》需在子线程中调用《《《《《《《《 + * 获取图片的file文件 + */ + @WorkerThread + fun loadGetFile(context: Context, url: String?): File? { + val glideConfig = getGlideConfig(context) ?: return null + return glideConfig.asFile().load(url).submit().get() + } + + /** + * 加载图片并设置format以及是否跳过内存缓存,无占位图 + */ + fun loadFormat( + path: String, + imageView: ImageView?, + format: DecodeFormat + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadFormat imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asBitmap().load(path).format(format).into(imageView) + } + } + + /** + * 加载图片并设置format以及是否跳过内存缓存,加载失败占位图 + */ + fun loadFormat( + path: String, + imageView: ImageView?, + @DrawableRes errorRes: Int, + format: DecodeFormat + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadFormat imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .asBitmap().load(path).error(errorRes).format(format).into(imageView) + } + } + + /** + * 加载object类型图片并拉满全屏,设置加载错误占位图 + * @param path object 类型图片 + * @param errorRes 加载错误占位图 + */ + fun loadObjectFitCenter(path: Any, @DrawableRes errorRes: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadObjectFitCenter imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(path).error(errorRes).fitCenter().dontAnimate() + .into(imageView) + } + } + + + /** + * 加载object类型图片,并设置错误资源占位图 + */ + fun loadObject(path: Any, imageView: ImageView?, @DrawableRes errorRes: Int) { + if (imageView == null) { + LibLogger.error(TAG, "loadObject imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .load(path) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .error(errorRes) + .into(imageView) + } + } + + /** + * 加载object类型图片,无占位图 + */ + fun loadObject(context: Context, path: Any, target: Target) { + getGlideConfig(context)?.apply { + this.asBitmap() + .load(path) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .dontAnimate() + .into(target) + } + } + + /** + * 加载object类型图片,错误占位图,设置缩略图 + */ + fun loadObject( + context: Context, + path: Any, + @DrawableRes errorRes: Int, + thumbnail: Float, + target: Target + ) { + val config = + getGlideConfig(context)?.load(path)?.diskCacheStrategy(DiskCacheStrategy.RESOURCE) + ?.error(errorRes) + if (thumbnail > 0) { + config?.thumbnail(thumbnail) + } + config?.into(target) + } + + /** + * 加载图片,返回Drawable + */ + fun loadForDrawable( + context: Context, + url: String, + @DrawableRes errorRes: Int, + target: Target + ) { + val config = + getGlideConfig(context)?.load(url)?.diskCacheStrategy(DiskCacheStrategy.RESOURCE) + ?.error(errorRes) + config?.into(target) + } + + /** + * 加载object类型图片,无占位图 + */ + fun loadObject( + path: Any, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadObject imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asBitmap() + .load(path) + .transform(*transformation) + .placeholder(defaultRes) + .error(defaultRes) + .into(imageView) + } + } + + /** + * 加载Uri图片时显示过度动画 + * @param uri 图片路径Uri + * + */ + fun loadUriCrossFade(uri: Uri, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadUriCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(uri) + .transition(DrawableTransitionOptions.withCrossFade()).into(imageView) + } + } + + /** + * 加载本地图片并添加过度动画以及图片变换 + */ + fun loadResCrossFade( + @DrawableRes resId: Int, + imageView: ImageView?, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadResCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId).diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transform(*transformation).transition(DrawableTransitionOptions.withCrossFade()) + .into(imageView) + } + } + + /** + * 加载本地图片并添加过度动画以及图片变换 + */ + fun loadResCrossFade( + @DrawableRes resId: Int, + imageView: ImageView?, + format: DecodeFormat, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadResCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId).diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transition(DrawableTransitionOptions.withCrossFade()).transform(*transformation) + .format(format) + .into(imageView) + } + } + + /** + * 加载Uri Gift图片,无默认图无动画 + * @param uri gift路径 + */ + fun loadUriGift(uri: Uri, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadUriGift imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asGif().load(uri).into(imageView) + } + } + + /** + * 加载Uri Gift图片并添加图片过度动画,无默认图无动画 + * @param uri gift路径 + */ + fun loadUriGiftAndCrossFade(uri: Uri, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadUriGift imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asGif().load(uri) + .transition(DrawableTransitionOptions.withCrossFade()).into(imageView) + } + } + + + /** + * 加载图片时显示过度动画并设置图形变换,缓存模式,无占位图 + * @param url 图片地址 + */ + fun loadCrossFade( + url: String?, + imageView: ImageView?, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .transition(DrawableTransitionOptions.withCrossFade()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE).dontAnimate() + .transform(*transformation).into(imageView) + } + } + + /** + * 加载图片时显示过度动画并设置图形变换,缓存模式 + * @param url 图片地址 + */ + fun loadCrossFade( + url: String?, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .transition(DrawableTransitionOptions.withCrossFade()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE).dontAnimate() + .placeholder(defaultRes).error(defaultRes) + .transform(*transformation).into(imageView) + } + } + + /** + * 加载图片时显示过度动画并设置图形变换,有占位图 + * @param url 图片地址 + * @param defaultRes 占位图 + * @param listener 请求监听 + */ + fun loadCrossFade( + url: String?, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + listener: RequestListener + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).placeholder(defaultRes).error(defaultRes) + .transition(DrawableTransitionOptions.withCrossFade()).listener(listener) + .into(imageView) + } + } + + /** + * 加载图片时显示过度动画并设置请求优先级,加载失败占位图 + * @param url 图片地址 + * @param errorRes 占位图 + * @param priority 优先级 + */ + fun loadCrossFade( + url: String?, + imageView: ImageView?, + @DrawableRes errorRes: Int, + priority: Priority + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).error(errorRes) + .transition(DrawableTransitionOptions.withCrossFade()).priority(priority) + .into(imageView) + } + } + + /** + * 加载图片时显示过度动画并设置请求优先级,加载失败占位图 + * @param url 图片地址 + * @param errorRes 占位图 + * @param priority 优先级 + * @param transformation 图形变换 + */ + fun loadCrossFade( + url: String?, + imageView: ImageView?, + @DrawableRes errorRes: Int, + priority: Priority, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadCrossFade imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).error(errorRes) + .transition(DrawableTransitionOptions.withCrossFade()).priority(priority) + .transform(*transformation) + .into(imageView) + } + } + + /** + * 加载图片时显示过度动画并设置图形变换,无占位图 + * @param url 图片地址 + */ + fun loadCrossFade( + context: Context, + url: String?, + target: Target, + vararg transformation: Transformation? + ) { + + getGlideConfig(context)?.apply { + this.asBitmap().load(url) + .transition(BitmapTransitionOptions.withCrossFade()) + .transform(*transformation) + .into(target) + } + } + + /** + * 加载需要优先级的图片并添加缓存模式,默认有动画,无占位图 + * @param priority 加载优先级 + */ + fun loadPriority( + url: String?, + imageView: ImageView?, + priority: Priority + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadPriority imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .dontAnimate().priority(priority).into(imageView) + } + } + + /** + * 加载需要优先级的图片,并设置磁盘缓存,无默认动画,无占位图, + * @param priority 加载有玄机 + */ + fun loadPriority( + context: Context, + url: String?, + priority: Priority, + target: Target + ) { + getGlideConfig(context)?.apply { + this.asBitmap().load(url).dontAnimate().priority(priority) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(target) + } + } + + /** + * 加载需要优先级的图片,并设置磁盘缓存,无默认动画,有加载失败占位图, + * @param priority 加载优先级 + */ + fun loadPriority( + context: Context, + url: String?, + @DrawableRes errorRes: Int, + priority: Priority, + target: Target + ) { + getGlideConfig(context)?.apply { + this.asBitmap().load(url) + .dontAnimate().priority(priority) + .error(errorRes) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + /** + * 加载需要优先级的图片并添加缓存模式,默认有动画,有占位图 + * @param priority 加载优先级 + * @param transformation 图形变换模式 + * @param defaultRes 占位图 + */ + fun loadPriority( + url: String?, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + priority: Priority, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadPriority imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).placeholder(defaultRes).error(defaultRes) + .dontAnimate().priority(priority).transform(*transformation).into(imageView) + } + } + + /** + * 加载需要优先级的图片并添加缓存模式,默认有动画,有占位图 + * @param priority 加载优先级 + * @param defaultRes 占位图 + */ + fun loadPriority( + url: String?, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + priority: Priority + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadPriority imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url).placeholder(defaultRes).error(defaultRes) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .dontAnimate().priority(priority).into(imageView) + } + } + + /** + * 加载广告页 + * @param defaultRes 加载错误显示图片 + */ + fun loadAd( + context: Context, + url: String?, + @DrawableRes defaultRes: Int, + target: DrawableImageViewTarget + ) { + getGlideConfig(context)?.apply { + this.load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .error(defaultRes).dontTransform().dontAnimate().into(target) + } + } + + /** + * 加载指定大小的资源图片 + * @param width 图片宽 + * @param height 图片高 + */ + fun loadResBySize(imageView: ImageView?, @DrawableRes resId: Int, width: Int, height: Int) { + if (imageView == null) { + LibLogger.error(TAG, "loadSquareIcon imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId).override(width, height).into(imageView) + } + } + + /** + * 加载图片并设置优先级, + * @param priority 加载优先级 + */ + fun load( + context: Context, + url: String?, + @DrawableRes defaultRes: Int, + priority: Priority, + target: Target + ) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(url) + .error(defaultRes) + .dontAnimate() + .priority(priority) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + + /** + * 加载图片返回到target中,无占位图 + */ + fun load( + context: Context, + url: String?, + target: Target, + vararg transformation: Transformation? + ) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transform(*transformation) + .into(target) + } + } + + /** + * 加载图片返回到target中,无占位图 + */ + fun load(context: Context, url: String?, target: Target) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(url) + .into(target) + } + } + + /** + * 加载图片并设置缩略图,占位图以及图形变换 + */ + fun load( + context: Context, + url: String?, + @DrawableRes errorRes: Int, + thumbnail: Float, + target: Target, + vararg transformation: Transformation? + ) { + getGlideConfig(context)?.apply { + this.load(url) + .placeholder(errorRes) + .thumbnail(thumbnail) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transform(*transformation) + .into(target) + } + } + + fun load( + context: Context, + url: String?, + @DrawableRes defaultRes: Int, + target: Target + ) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(url) + .placeholder(defaultRes) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + fun load( + context: Context, + mrcImageUrl: Any?, + @DrawableRes defaultDrawable: Int, + target: Target + ) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(mrcImageUrl) + .placeholder(defaultDrawable) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + /** + * 加载本地文件,并设置宽高以及图形变换 + */ + fun loadLocalFile( + filePath: String, + @DrawableRes errorRes: Int, + imageView: ImageView?, + width: Int, + height: Int, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadLocalFile imageView is null") + return + } + val file = File(filePath) + getGlideConfig(imageView.context)?.apply { + this + .load(file) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .error(errorRes) + .override(width, height) + .dontAnimate() + .transform(*transformation) + .into(imageView) + } + } + + /** + * 加载本地文件 + */ + fun loadLocalFile( + filePath: String, + @DrawableRes errorRes: Int, + imageView: ImageView?, + width: Int, + height: Int + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadLocalFile imageView is null") + return + } + val file = File(filePath) + getGlideConfig(imageView.context)?.apply { + this + .load(file) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .error(errorRes) + .override(width, height) + .dontAnimate() + .into(imageView) + } + } + + //加载文件,并传递target + fun loadLocalFile( + context: Context, + filePath: String, + @DrawableRes defaultDrawable: Int, + target: Target + ) { + val file = File(filePath) + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(file) + .placeholder(defaultDrawable) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + //加载文件 + fun loadLocalFile( + filePath: String?, + @DrawableRes defaultDrawable: Int, + imageView: ImageView? + ) { + if (imageView == null) + return + if (TextUtils.isEmpty(filePath)) { + LibLogger.error(TAG, "loadLocalFile url is empty.") + getGlideConfig(imageView.context)?.apply { + this + .load(defaultDrawable) + .placeholder(defaultDrawable) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } else { + val file = File(filePath) + getGlideConfig(imageView.context)?.apply { + this + .load(file) + .placeholder(defaultDrawable) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + } + + // 加载圆形头像--小图 + fun loadRoundIcon( + icon: String?, + imageView: ImageView?, + @DrawableRes defaultRes: Int, + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadRoundIcon imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .load(icon) + .dontAnimate() + .placeholder(defaultRes) + .error(defaultRes) + .priority(Priority.IMMEDIATE) + .transform(CircleCrop()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + // 加载圆形本地头像 + fun loadRoundIcon(resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadRoundIcon imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .load(resId) + .dontAnimate() + .priority(Priority.IMMEDIATE) + .transform(CircleCrop()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + // 加载本地图片,加载到Target + fun loadTargetLocalRes(context: Context, resId: Int, target: Target) { + getGlideConfig(context)?.apply { + this.asBitmap() + .load(resId) + .placeholder(resId) + .dontAnimate() + .into(target) + } + } + + //加载指定大小的图片 + fun loadFixSizeImage( + context: Context, + url: String?, + width: Int, + height: Int, + target: Target + ) { + getGlideConfig(context)?.apply { + this + .asBitmap() + .load(url) + .fitCenter() + .override(width, height) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(target) + } + } + + /** + * 加载本地资源GIF图 + */ + fun loadGiftRes(@DrawableRes resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadGiftRes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asGif().load(resId).into(imageView) + } + } + + /** + * 加载本地资源GIF图,并设置填充模式 + */ + fun loadGiftRes( + @DrawableRes resId: Int, + imageView: ImageView?, + anim: Int + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadGiftRes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asGif().load(resId).centerCrop() + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transition(GenericTransitionOptions.with(anim)).into(imageView) + } + } + + /** + * 加载GIF图片,无占位图和动画 + */ + fun loadGift(url: String?, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadGif imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asGif().load(url).into(imageView) + } + } + + /** + * 加载GIF图 + * @param resId 占位图 + * @param anim 显示动画 + */ + fun loadGif( + url: String?, + @DrawableRes resId: Int, + anim: Int, + imageView: ImageView? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadGif imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .asGif() + .load(url).placeholder(resId).error(resId) + .transition(GenericTransitionOptions.with(anim)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop().into(imageView) + } + } + + fun loadGifImage( + imageView: ImageView?, + url: Any, + isCenterCrop: Boolean, + isScale: Boolean, + corner: Float, + maxWidth: Int, + maxHeight: Int, + callBack: IGlideLoaderCallBack? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadGifImage imageView is null") + return + } + val builder = ComplexTransformation.ComplexParamsBuilder() + .setMaxHeight(maxHeight) + .setMaxWidth(maxWidth) + .setIsNeedCropCenter(isCenterCrop) + .setCorner(corner) + .setIsNeedCorner(true) + .setIsNeedScale(isScale) + getGlideConfig(imageView.context)?.apply { + this + .load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transform(ComplexTransformation(builder)) + .override(maxWidth, maxHeight) + .addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + LibLogger.error(TAG, "loadGifImage url:$url, onException:$e") + callBack?.onError() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + callBack?.onSuccess() + return false + } + }) + .into(imageView) + } + } + + /** + * 原图片宽高短的一边设置为指定目标值,长的自适应拉伸,按照指定宽高比例和截取方式进行截取 + */ + fun loadAssignScaleImage( + imageView: ImageView?, + url: Any, + targetSize: Int, + whRadio: Float, + clipType: AssignScaleTransformation.ClipType?, + callBack: IGlideLoaderCallBack? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadAssignScaleImage imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .thumbnail(0.1f) + .transform(AssignScaleTransformation(targetSize, whRadio, clipType!!)) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + LibLogger.error( + TAG, + "loadAssignScaleImage url:$url, onException:$e" + ) + callBack?.onError() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + callBack?.onSuccess() + return false + } + }) + .into(imageView) + } + } + + + /** + * 加载圆形图片,并设置默认图 + */ + fun loadCircleImage(url: String?, @DrawableRes resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadCircleImage imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .load(url) + .placeholder(resId) + .dontAnimate() + .transform(CircleCrop()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + //加载上半圆角图片 + fun loadHalfConnerImage( + url: String?, + @DrawableRes resId: Int, + radius: Float, + imageView: ImageView? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadHalfConnerImage imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this + .asBitmap() + .load(url) + .placeholder(resId) + .error(resId) + .transform( + CenterCrop(), RoundedCornersTransformation( + Utils.dip2px(imageView.context, radius), + 0, RoundedCornersTransformation.CornerType.TOP + ) + ).into(imageView) + } + } + + + // 加载圆角图片 + fun loadConnerImage( + url: String?, + @DrawableRes resId: Int, + radius: Float, + imageView: ImageView? + ) { + if (imageView == null) { + LibLogger.error(TAG, "ImageLoader ImageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asBitmap().load(url) + .placeholder(resId) + .error(resId) + .transform( + CenterCrop(), RoundedCornersTransformation( + Utils.dip2px(imageView.context, radius), + 0, RoundedCornersTransformation.CornerType.ALL + ) + ).into(imageView) + } + } + + + // 加载本地资源图片 + fun loadLocalRes(@DrawableRes resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadLocalRes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId) + .dontAnimate() + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + /** + * 加载本地资源,设置缓存模式,设置加载格式,无占位图 + * @param format 图片加载格式 + */ + fun loadLocalRes( + @DrawableRes resId: Int, + imageView: ImageView?, + format: DecodeFormat + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadLocalRes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId) + .format(format) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + + } + + /** + * 加载本地资源 + * @param resId 资源ID + * @param transformation 图片转换器 + */ + fun loadLocalRes( + @DrawableRes resId: Int, + imageView: ImageView?, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "loadLocalRes imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId).diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .transform(*transformation).into(imageView) + } + + } + + + // 加载Assets图片 + fun loadAssetsImg(path: String?, imageView: ImageView?) { + if (imageView == null || TextUtils.isEmpty(path)) { + LibLogger.error(TAG, "loadAssetsImg path is invalid or imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(path) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + /** + * 加载res资源,并设置居中裁剪 + */ + fun loadResCenterCrop(@DrawableRes resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadResScale imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId) + .centerCrop() + .into(imageView) + } + } + + /** + * 加载res资源,并设置居中裁剪 + */ + fun loadResFitCenter(@DrawableRes resId: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "loadResFitCenter imageView is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(resId) + .fitCenter() + .into(imageView) + } + } + + + /** + * 加载url图片并拉满全屏,无占位图,添加磁盘缓存 + */ + fun loadFitCenter(url: String, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, " loadFitCenter imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .dontAnimate() + .fitCenter() + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + + /** + * 加载url图片并拉满全屏,添加加载错误占位图 + */ + fun loadFitCenter(url: String?, @DrawableRes errorRes: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, " loadFitCenter imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .error(errorRes).fitCenter().dontAnimate().into(imageView) + } + } + + /** + * 加载url图片并拉满全屏,添加加载错误占位图 + */ + fun loadFitCenter( + context: Context, + url: String?, + width: Int, + height: Int, + listener: RequestListener + ) { + + getGlideConfig(context)?.apply { + this.load(url) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .addListener(listener) + .fitCenter().dontAnimate().into(width, height) + } + } + + + /** + * 通过target加载图片,并撑满全屏 + */ + fun loadFitCenterIntoTarget(context: Context, url: String, target: Target) { + getGlideConfig(context)?.apply { + this.asBitmap().load(url).dontAnimate().fitCenter() + .into(target) + } + } + + /** + * 加载图片,是否跳过缓存,以及设置图片变换,无占位图 + */ + fun load( + url: String?, + imageView: ImageView?, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "load imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .transform(*transformation) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .dontAnimate() + .into(imageView) + } + } + + /** + * 加载图片,是否跳过缓存,以及设置图片变换,无占位图 + */ + fun load( + url: String?, + imageView: ImageView?, + @DrawableRes resId: Int, + vararg transformation: Transformation? + ) { + if (imageView == null) { + LibLogger.error(TAG, "load imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .placeholder(resId) + .error(resId) + .transform(*transformation) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .dontAnimate() + .into(imageView) + } + } + + + /** + * 使用Glide下载图片,返回File + */ + fun downloadFromUrl( + context: Context?, + url: String?, + listener: RequestListener? + ) { + if (context == null) { + LibLogger.error(TAG, "load context is null") + return + } + + val glideConfig = getGlideConfig(context) + glideConfig?.downloadOnly()?.load(url)?.listener(listener)?.preload() + } + + /** + * 使用Glide下载图片,返回File + */ + fun downloadFromUrl2( + context: Context?, + url: String?, + listener: RequestListener? + ) { + if (context == null) { + LibLogger.error(TAG, "load context is null") + return + } + + val glideConfig = getGlideConfig(context) + glideConfig + ?.asFile() + ?.load(url) + ?.addListener(listener) + ?.submit() + } + + /** + * 使用Glide下载图片,返回File + */ + fun downloadFromUrlToDrawable( + context: Context?, + url: String?, + listener: RequestListener? + ) { + if (context == null) { + LibLogger.error(TAG, "load context is null") + return + } + + val glideConfig = getGlideConfig(context) + glideConfig + ?.asDrawable() +// ?.skipMemoryCache(true) + ?.load(url) + ?.addListener(listener) + ?.submit() + } /** + * 使用Glide下载图片,返回File + */ + fun downloadFromUrlToBitmap( + context: Context?, + url: String?, + listener: RequestListener? + ) { + if (context == null) { + LibLogger.error(TAG, "load context is null") + return + } + + val glideConfig = getGlideConfig(context) + glideConfig + ?.asBitmap() +// ?.skipMemoryCache(true) + ?.load(url) + ?.addListener(listener) + ?.submit() + } + + /** + * 加载图片,并添加RequestListener ,无占位图 ,listener需使用addListener方法添加 + */ + fun load(url: String?, imageView: ImageView?, listener: RequestListener) { + if (imageView == null || TextUtils.isEmpty(url)) { + LibLogger.error(TAG, "load imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .addListener(listener) + .into(imageView) + } + } + + /** + * 加载图片,并添加RequestListener ,无占位图 + */ + fun load( + url: String?, + imageView: ImageView?, + format: DecodeFormat, + listener: RequestListener + ) { + if (imageView == null || TextUtils.isEmpty(url)) { + LibLogger.error(TAG, "load imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asBitmap().load(url).addListener(listener).format(format) + .into(imageView) + } + } + + //加载图片,无默认图片 + fun load(url: String?, imageView: ImageView?) { + if (imageView == null || TextUtils.isEmpty(url)) { + LibLogger.error(TAG, "load url is invalid or imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .dontAnimate() + .into(imageView) + } + } + //加载图片,无默认图片 + fun loadAsBitmap(url: String?, imageView: ImageView?) { + if (imageView == null || TextUtils.isEmpty(url)) { + LibLogger.error(TAG, "load url is invalid or imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.asDrawable().load(url) + .into(imageView) + } + } + + //加载图片,无默认图片 + fun loadWithError(url: String?, errorDrawable: Int, imageView: ImageView?) { + if (imageView == null || TextUtils.isEmpty(url)) { + LibLogger.error(TAG, "load url imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .dontAnimate() + .error(errorDrawable) + .into(imageView) + } + } + + /** + * 加载图片病设置占位图 + */ + fun load(url: String?, @DrawableRes defaultRes: Int, imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "load url is invalid or imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .placeholder(defaultRes).error(defaultRes) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + fun load(url: String?, @DrawableRes defaultRes: Int, @DrawableRes errorRes: Int,imageView: ImageView?) { + if (imageView == null) { + LibLogger.error(TAG, "load url is invalid or imageViw is null") + return + } + getGlideConfig(imageView.context)?.apply { + this.load(url) + .placeholder(defaultRes).error(errorRes) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(imageView) + } + } + private fun getGlideConfig(context: Context): RequestManager? { + if (checkActivityIsDestroy(context)) return null + + return Glide.with(context) + } + + /** + * 判断activity是否回收 + * @return true 被回收 ,false 没有被回收 + */ + private fun checkActivityIsDestroy(context: Context): Boolean { + if (context is Application) { + return false + } else if (context is Activity) { + return !ActivityHelper.isCanUse(context) + } else if (context is ContextWrapper) { + val baseContext = context.baseContext + if (baseContext is Activity) { + return !ActivityHelper.isCanUse(baseContext) + } + } + return true + } + + interface IGlideLoaderCallBack { + fun onSuccess() + fun onError() + } + + + companion object { + private const val TAG = "GlideUtils" + + // 0小图(圆形)0=120*120; + const val SMALL_CIRCLE = 0 + + // 1大图(长方形)800*600 + const val BIG_RECTANGLE = 1 + + // 2(正方形)300*300 + const val BIG_SQUARE = 2 + + private val glideUtils by lazy { GlideUtils() } + + @JvmStatic + fun instance(): GlideUtils { + return glideUtils + } + } + + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/transform/AssignScaleTransformation.kt b/library/src/module_common/java/com/chwl/library/common/transform/AssignScaleTransformation.kt new file mode 100644 index 0000000..455ec24 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/transform/AssignScaleTransformation.kt @@ -0,0 +1,96 @@ +package com.chwl.library.common.transform + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.RectF +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest + +/** + * author: lishangming + * e-mail: lishangming@miya818.com + * time: 2021/09/17 + * desc: glide转化器,原图片宽高短的一边设置为指定目标值,长的自适应拉伸,按照指定宽高比例和截取方式进行截取 + */ +class AssignScaleTransformation(private val targetSize: Int, private val whRadio: Float, private val clipType: ClipType) : BitmapTransformation() { + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(("AssignScaleTransformation(${targetSize}_${whRadio}_${clipType})").toByteArray()) + } + + override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { + return crop(pool, toTransform) ?: toTransform + } + + private fun crop(pool: BitmapPool, source: Bitmap?): Bitmap? { + if (targetSize <= 0 || whRadio <= 0) return null + if (source == null) return null + //计算截取原文件的宽高 + var clipWidth = 0 + var clipHeight = 0 + when { + source.width / whRadio <= source.height -> { + //用宽度计算,按比例拉伸高度比原本的小,故宽度不变,高度按比例设置 + clipWidth = source.width + clipHeight = (source.width / whRadio).toInt() + } + source.height * whRadio <= source.width -> { + //用高度计算,按比例拉伸宽度比原本的小,故高度不变,宽度按比例设置 + clipWidth = (source.height * whRadio).toInt() + clipHeight = source.height + + } + else -> { + clipWidth = source.width + clipHeight = source.height + } + } + + //需要生成图片的宽高 + val resultWidth = targetSize + val resultHeight = (targetSize / whRadio).toInt() + //截取比例 + val left = if (clipWidth < source.width) (source.width - clipWidth) / 2 else 0 + val right = left + clipWidth + var result: Bitmap? = pool[resultWidth, resultHeight, Bitmap.Config.ARGB_8888] + if (result == null) { + result = Bitmap.createBitmap(resultWidth, resultHeight, Bitmap.Config.ARGB_8888) + } + val targetRect = RectF(0f, 0f, resultWidth.toFloat(), resultHeight.toFloat()) + val sourceRect = when (clipType) { + ClipType.TOP -> { + //顶部截取,固定截取高度 + Rect(left, 0, right, clipHeight) + } + else -> { + //默认中间截取 + val top = if (clipHeight < source.height) (source.height - clipHeight) / 2 else 0 + val bottom = top + clipHeight + Rect(left, top, right, bottom) + } + } + if (result != null) { + val canvas = Canvas(result) + if (clipWidth < source.width || clipHeight < source.height) { + canvas.drawBitmap(source, sourceRect, targetRect, null) + } else { + canvas.drawBitmap(source, null, targetRect, null) + } + } + return result + } + + override fun equals(obj: Any?): Boolean { + return obj is AssignScaleTransformation + } + + override fun hashCode(): Int { + return javaClass.name.hashCode() + } + + enum class ClipType { + TOP,//截取顶部 + CENTER,//截取中间 + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/transform/ComplexTransformation.java b/library/src/module_common/java/com/chwl/library/common/transform/ComplexTransformation.java new file mode 100644 index 0000000..faa10f1 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/transform/ComplexTransformation.java @@ -0,0 +1,152 @@ +package com.chwl.library.common.transform; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; + +import java.security.MessageDigest; + +/** + * Created by zhl on 2020/1/3. + */ +public class ComplexTransformation extends BitmapTransformation { + + private ComplexParamsBuilder mBuilder; + + public ComplexTransformation(@NonNull ComplexParamsBuilder builder) { + mBuilder = builder; + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { + return crop(pool, toTransform, mBuilder); + } + + private static Bitmap crop(BitmapPool pool, Bitmap source, ComplexParamsBuilder builder) { + if (source == null) return null; + + int resultWidth = builder.mMaxWidth == 0 ? source.getWidth() : builder.mMaxWidth; + int resultHeight = builder.mMaxHeight == 0 ? source.getHeight() : builder.mMaxHeight; + + if (builder.mIsNeedScale) { + if ((source.getWidth() > resultWidth || source.getHeight() > resultHeight)) { + float scaleX = (float) resultWidth / source.getWidth(); + float scaleY = (float) resultHeight / source.getHeight(); + float scale = Math.min(scaleX, scaleY); + resultWidth = (int) (scale * source.getWidth()); + resultHeight = (int) (scale * source.getHeight()); + } else { + resultWidth = source.getWidth(); + resultHeight = source.getHeight(); + } + } + + Bitmap result = pool.get(resultWidth, resultHeight, Bitmap.Config.ARGB_8888); + + RectF targetRect = new RectF(0, 0, resultWidth, resultHeight); + Canvas canvas = new Canvas(result); + if (builder.mIsNeedCorner && source.getWidth() == resultWidth && source.getHeight() == resultHeight) { + //该图没有过缩放,可直接绘制圆角 + Paint paint = new Paint(); + paint.setShader(new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); + paint.setAntiAlias(true); + canvas.drawRoundRect(targetRect, builder.mCorner, builder.mCorner, paint); + return result; + } + if (builder.mIsNeedCropCenter && source.getHeight() != source.getWidth()) { + int min = Math.min(source.getWidth(), source.getHeight()); + Rect sourceRect = new Rect((source.getWidth() - min) / 2, + (source.getHeight() - min) / 2, + (source.getWidth() - min) / 2 + min, + (source.getHeight() - min) / 2 + min); + canvas.drawBitmap(source, sourceRect, targetRect, null); + } else { + canvas.drawBitmap(source, null, targetRect, null); + } + if (builder.mIsNeedCorner) { + Paint paint = new Paint(); + paint.setShader(new BitmapShader(result, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP)); + paint.setAntiAlias(true); + Bitmap cornerResult = pool.get(resultWidth, resultHeight, Bitmap.Config.ARGB_8888); + Canvas cornerCanvas = new Canvas(cornerResult); + cornerCanvas.drawRoundRect(targetRect, builder.mCorner, builder.mCorner, paint); + return cornerResult; + } + return result; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(("ComplexTransformation(" + mBuilder.toString() + ")").getBytes()); + } + + public static class ComplexParamsBuilder { + int mMaxWidth; + int mMaxHeight; + boolean mIsNeedScale; + boolean mIsNeedCropCenter; + boolean mIsNeedCorner; + float mCorner; + + public ComplexParamsBuilder setMaxWidth(int mMaxWidth) { + this.mMaxWidth = mMaxWidth; + return this; + } + + public ComplexParamsBuilder setMaxHeight(int mMaxHeight) { + this.mMaxHeight = mMaxHeight; + return this; + } + + public ComplexParamsBuilder setIsNeedScale(boolean mIsNeedScale) { + this.mIsNeedScale = mIsNeedScale; + return this; + } + + public ComplexParamsBuilder setIsNeedCropCenter(boolean mIsNeedCropCenter) { + this.mIsNeedCropCenter = mIsNeedCropCenter; + return this; + } + + public ComplexParamsBuilder setIsNeedCorner(boolean mIsNeedCorner) { + this.mIsNeedCorner = mIsNeedCorner; + return this; + } + + public ComplexParamsBuilder setCorner(float mCorner) { + this.mCorner = mCorner; + return this; + } + + @Override + public String toString() { + return "ComplexParamsBuilder{" + + "mMaxWidth=" + mMaxWidth + + ", mMaxHeight=" + mMaxHeight + + ", mIsNeedScale=" + mIsNeedScale + + ", mIsNeedCropCenter=" + mIsNeedCropCenter + + ", mIsNeedCorner=" + mIsNeedCorner + + ", mCorner=" + mCorner + + '}'; + } + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ComplexTransformation; + } + + @Override + public int hashCode() { + return getClass().getName().hashCode(); + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/ActivityHelper.java b/library/src/module_common/java/com/chwl/library/common/util/ActivityHelper.java new file mode 100644 index 0000000..f4154af --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ActivityHelper.java @@ -0,0 +1,48 @@ +package com.chwl.library.common.util; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.view.ContextThemeWrapper; +import android.view.View; + +import androidx.appcompat.widget.TintContextWrapper; + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: 判断Activity是否存在 + */ +public class ActivityHelper { + + /** + * 检查Activity是否可用 + */ + public static boolean isCanUse(Activity activity) { + return activity != null && !activity.isFinishing() && !activity.isDestroyed(); + } + + /** + * 从View里面获取Activity对象 + */ + public static Activity getActivityFromView(View view) { + Context context = view.getContext(); + return getActivityFromContext(context); + } + + /** + * 从Context里面获取Activity对象 + */ + public static Activity getActivityFromContext(Context context) { + if (context instanceof Activity) { + return (Activity) context; + } else if (context instanceof ContextThemeWrapper && ((ContextThemeWrapper) context).getBaseContext() instanceof Activity) { + return (Activity) ((ContextWrapper) context).getBaseContext(); + } else if (context instanceof TintContextWrapper) { + return (Activity) ((ContextWrapper) context).getBaseContext(); + } else { + return null; + } + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/AlbumUtils.kt b/library/src/module_common/java/com/chwl/library/common/util/AlbumUtils.kt new file mode 100644 index 0000000..ab15f4e --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/AlbumUtils.kt @@ -0,0 +1,313 @@ +package com.chwl.library.common.util + +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.chwl.library.common.application.BaseApp +import com.chwl.library.common.file.FileHelper +import com.chwl.library.common.glide.GlideUtils +import java.io.* + +/** + * create by ysx 2020/9/25 0025 + *文件工具类,为了适配android10,11 + */ +object AlbumUtils { + + /** + * 兼容android Q + * 网络图片保存本地 + * 返回是否保存成功 + */ + fun addUrlToAlbum(context: Context?, url: String?, callback: (Boolean) -> Unit) { + if (url == null) { + callback.invoke(false) + return + } + getGlideImagePath(context, url) { glideImagePath -> + glideImagePath?.also { + val fileName: String + val imageType: String + if (glideImagePath.endsWith(".gif", true)) { + fileName = System.currentTimeMillis().toString() + ".gif" + imageType = "image/gif" + } else { + fileName = System.currentTimeMillis().toString() + ".jpg" + imageType = "image/jpeg" + } + + val values = ContentValues() + values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + values.put(MediaStore.MediaColumns.MIME_TYPE, imageType) + values.put(MediaStore.MediaColumns.SIZE, 1) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) + } else { + values.put( + MediaStore.MediaColumns.DATA, + "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$fileName" + ) + } + val contentResolver = BaseApp.getContext().contentResolver + val uri = + contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + if (uri != null) { + try { + val oldFile = File(glideImagePath) + if (oldFile.exists()) { //文件存在时 + val inStream = FileInputStream(glideImagePath) //读入原文件 + val outputStream = contentResolver.openOutputStream(uri) + if (outputStream != null) { + val bos = BufferedOutputStream(outputStream) + val buffer = ByteArray(1024) + var bytes = inStream.read(buffer) + while (bytes >= 0) { + bos.write(buffer, 0, bytes) + bos.flush() + bytes = inStream.read(buffer) + } + bos.close() + callback.invoke(true) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + callback.invoke(false) + } + + /** + * 兼容android Q + * 将bitmap保存到本地图库 + * displayName 文件名 "xxx.jpg" + * mimeType "image/jpeg" + */ + @JvmOverloads + @JvmStatic + fun addBitmapToAlbum( + bitmap: Bitmap?, + displayName: String = "${System.currentTimeMillis()}.jpg", + mimeType: String = "image/jpeg", + compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG + ): Boolean { + if (bitmap == null) return false + val values = ContentValues() + values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) + } else { + values.put( + MediaStore.MediaColumns.DATA, + "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName" + ) + } + var outputStream: OutputStream? = null + try { + val contentResolver = BaseApp.getContext().contentResolver + val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + if (uri != null) { + outputStream = contentResolver.openOutputStream(uri) + if (outputStream != null) { + bitmap.compress(compressFormat, 100, outputStream) + return true + } + } + } catch (e: Exception) { + } finally { + try { + outputStream?.close() + } catch (ex: Exception) { + } + } + return false + } + + /** + * 兼容android Q + * 本地缓存文件保存本地相册 + * 返回是否保存成功 + */ + fun addImageFileToAlbum(file: File?): Boolean { + if (file == null) return false + val start: Int = file.name.lastIndexOf(".") + val suffix: String = file.name.substring(start + 1) + val fileName: String + val imageType: String + if (suffix == "gif" || suffix == "GIF") { + fileName = System.currentTimeMillis().toString() + ".gif" + imageType = "image/gif" + } else { + fileName = System.currentTimeMillis().toString() + ".jpg" + imageType = "image/jpeg" + } + + val values = ContentValues() + values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + values.put(MediaStore.MediaColumns.MIME_TYPE, imageType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) + } else { + values.put( + MediaStore.MediaColumns.DATA, + "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$fileName" + ) + } + val contentResolver = BaseApp.getContext().contentResolver + val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + if (uri != null) { + var inStream: FileInputStream? = null + var outputStream: OutputStream? = null + var bos: BufferedOutputStream? = null + try { + if (file.exists()) { //文件存在时 + inStream = FileInputStream(file) //读入原文件 + outputStream = contentResolver.openOutputStream(uri) + if (outputStream != null) { + bos = BufferedOutputStream(outputStream) + val buffer = ByteArray(1024) + var bytes = inStream.read(buffer) + while (bytes >= 0) { + bos.write(buffer, 0, bytes) + bos.flush() + bytes = inStream.read(buffer) + } + return true + } + } + } catch (e: Exception) { + } finally { + try { + inStream?.close() + } catch (ex: Exception) { + } + try { + outputStream?.close() + } catch (ex: Exception) { + } + try { + bos?.close() + } catch (ex: Exception) { + } + } + } + return false + } + + /** + * Glide 获得图片路径 + */ + private fun getGlideImagePath(context: Context?, imgUrl: String, callback: (String?) -> Unit) { + try { + GlideUtils.instance().downloadFromUrl(context, imgUrl, object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + callback.invoke("") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + callback.invoke(resource?.absolutePath) + return false + } + }) + } catch (e: Exception) { + callback.invoke("") + } + } + + + /** + * 动态获得h5图片路径 + */ + fun getTrendImagePath(context: Context?, imgUrl: String, callback: (String?) -> Unit) { + try { + GlideUtils.instance().downloadFromUrl(context, imgUrl, object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + callback.invoke("") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + val fileDir = + FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath + "/trend/test" + //获取到下载得到的图片,进行本地保存 + //第二个参数为你想要保存的目录名称 + val appDir = File(fileDir) + if (!appDir.exists()) { + appDir.mkdirs() + } + val fileName = System.currentTimeMillis().toString() + ".jpg" + val destFile = File(appDir, fileName) + //把gilde下载得到图片复制到定义好的目录中去 + copy(resource, destFile) { + callback.invoke(destFile.absolutePath) + } + return false + } + }) + } catch (e: Exception) { + callback.invoke("") + } + } + + /** + * 复制文件 + * + * @param source 输入文件 + * @param target 输出文件 + */ + fun copy(source: File?, target: File?, callback: () -> Unit) { + var fileInputStream: FileInputStream? = null + var fileOutputStream: FileOutputStream? = null + try { + fileInputStream = FileInputStream(source) + fileOutputStream = FileOutputStream(target) + val buffer = ByteArray(1024) + while (fileInputStream.read(buffer) > 0) { + fileOutputStream.write(buffer) + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } finally { + try { + fileInputStream?.close() + fileOutputStream?.close() + callback.invoke() + } catch (e: IOException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/ClickUtils.kt b/library/src/module_common/java/com/chwl/library/common/util/ClickUtils.kt new file mode 100644 index 0000000..a327501 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ClickUtils.kt @@ -0,0 +1,76 @@ +package com.chwl.library.common.util + +import android.view.View + +object ClickUtils { + /*** + * 设置延迟时间的View扩展 + * @param delay Long 延迟时间,默认600毫秒 + * @return T + */ + fun T.withTrigger(delay: Long = 600): T { + triggerDelay = delay + return this + } + + /*** + * 点击事件的View扩展 + * @param block: (T) -> Unit 函数 + * @return Unit + */ + fun T.click(block: (T) -> Unit) = setOnClickListener { + block(it as T) + } + + /*** + * 带延迟过滤的点击事件View扩展 + * @param delay Long 延迟时间,默认600毫秒 + * @param block: (T) -> Unit 函数 + * @return Unit + */ + fun T.clickWithTrigger(time: Long = 600, block: (T) -> Unit) { + triggerDelay = time + setOnClickListener { + if (clickEnable()) { + block(it as T) + } + } + } + + private var T.triggerLastTime: Long + get() = if (getTag(1123460103) != null) getTag(1123460103) as Long else -601 + set(value) { + setTag(1123460103, value) + } + + private var T.triggerDelay: Long + get() = if (getTag(1123461123) != null) getTag(1123461123) as Long else 600 + set(value) { + setTag(1123461123, value) + } + + private fun T.clickEnable(): Boolean { + var flag = false + val currentClickTime = System.currentTimeMillis() + if (currentClickTime - triggerLastTime >= triggerDelay) { + flag = true + triggerLastTime = currentClickTime + } + return flag + } + + /*** + * 带延迟过滤的点击事件监听,见[View.OnClickListener] + * 延迟时间根据triggerDelay获取:600毫秒,不能动态设置 + */ + interface OnLazyClickListener : View.OnClickListener { + + override fun onClick(v: View?) { + if (v?.clickEnable() == true) { + onLazyClick(v) + } + } + + fun onLazyClick(v: View) + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/Config.java b/library/src/module_common/java/com/chwl/library/common/util/Config.java new file mode 100644 index 0000000..7493d65 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/Config.java @@ -0,0 +1,195 @@ +package com.chwl.library.common.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Parcelable; + +import com.tencent.mmkv.MMKV; + +import java.util.Set; + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: 使用腾讯MMKV框架替代SharedPreference,参考文档:https://github.com/Tencent/MMKV/wiki/android_tutorial_cn + */ +public class Config { + private volatile static Config instance = null; + private MMKV mmkv; + + public static Config getInstance(Context context) { + if (instance == null) { + synchronized (Config.class) { + if (instance == null) { + instance = new Config(context); + } + } + } + return instance; + } + + private Config(Context context) { + try { + if (context == null) { + return; + } + MMKV.initialize(context); + mmkv = MMKV.mmkvWithID("config", MMKV.MULTI_PROCESS_MODE); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void setOnChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + if (listener != null && mmkv != null) { + mmkv.registerOnSharedPreferenceChangeListener(listener); + } + } + + /************************************** 编码方法 **************************************/ + + public boolean putBytes(String key, byte[] bytes) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, bytes); + } + + public boolean putInt(String key, int value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putLong(String key, long value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putFloat(String key, float value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putDouble(String key, double value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putBoolean(String key, boolean value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putString(String key, String value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putStringSet(String key, Set value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + public boolean putParcelable(String key, Parcelable value) { + if (mmkv == null) { + return false; + } + return mmkv.encode(key, value); + } + + /************************************** 解码方法 **************************************/ + + public byte[] getBytes(String key, byte[] defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeBytes(key, defaultValue); + } + + public int getInt(String key, int defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeInt(key, defaultValue); + } + + public long getLong(String key, long defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeLong(key, defaultValue); + } + + public float getFloat(String key, float defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeFloat(key, defaultValue); + } + + public double getDouble(String key, double defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeDouble(key, defaultValue); + } + + public boolean getBoolean(String key, boolean defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeBool(key, defaultValue); + } + + public String getString(String key, String defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeString(key, defaultValue); + } + + public Set getStringSet(String key, Set defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeStringSet(key, defaultValue); + } + + public T getParcelable(String key, Class tClass, T defaultValue) { + if (mmkv == null) { + return defaultValue; + } + return mmkv.decodeParcelable(key, tClass, defaultValue); + } + + /************************************** 清理方法 **************************************/ + + public void remove(String key) { + if (mmkv == null) { + return; + } + mmkv.remove(key); + } + + public void clearAll() { + if (mmkv == null) { + return; + } + mmkv.clearAll(); + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/CoreUtils.java b/library/src/module_common/java/com/chwl/library/common/util/CoreUtils.java new file mode 100644 index 0000000..7aeba25 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/CoreUtils.java @@ -0,0 +1,59 @@ +package com.chwl.library.common.util; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.EventBusException; + +public class CoreUtils { + private static final String TAG = "CoreUtils"; + private static EventBus sEventBus; + + public static void send(T moduleCallback) { + if (moduleCallback == null) { + LibLogger.warn(TAG, "moduleCallback == null"); + return; + } + try { + CoreUtils.getEventBus().post(moduleCallback); + } catch (Exception e) { + LibLogger.error(TAG, "EventBus exception", e); + } + } + + public static void register(T receiver) { + try { + if (!CoreUtils.getEventBus().isRegistered(receiver)) { + CoreUtils.getEventBus().register(receiver); + } + } catch (EventBusException e) { + //ignore + } catch (Exception e) { + LibLogger.error(TAG, "register error", e); + } + } + + public static void unregister(T receiver) { + try { + if (CoreUtils.getEventBus().isRegistered(receiver)) { + CoreUtils.getEventBus().unregister(receiver); + } + } catch (EventBusException e) { + //ignore + } catch (Exception e) { + LibLogger.error(TAG, "unregister error", e); + } + } + + private static EventBus getEventBus() { + if (sEventBus == null) { + synchronized (CoreUtils.class) { + if (sEventBus == null) { + sEventBus = EventBus.builder().throwSubscriberException(false) + .logSubscriberExceptions(false).logNoSubscriberMessages(false) + .build(); + } + } + } + return sEventBus; + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/DeviceUtil.kt b/library/src/module_common/java/com/chwl/library/common/util/DeviceUtil.kt new file mode 100644 index 0000000..731a2ef --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/DeviceUtil.kt @@ -0,0 +1,336 @@ +package com.chwl.library.common.util + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import android.os.Process +import android.provider.Settings +import android.telephony.TelephonyManager +import android.text.TextUtils +import com.chwl.library.common.application.BaseApp +import java.io.RandomAccessFile +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * create by wushaocheng 2023/2/15 + * 获取设备id + */ +object DeviceUtil { + private const val TAG = "DeviceUtils" + private const val ANDROID_ID_KEY = "android_id" + private const val cache_device_id = "cache_device_id" + private var deviceId = "" + private var mcc_mnc = "" + private var mcc = "" + private var mnc = "" + @Volatile + private var ANDROID_ID = "" + + /** + * 获取设备信息 + */ + var deviceDesc = "" + get() { + if (!TextUtils.isEmpty(field)) { + return field + } + field = + String.format("%s-%s-%s", Build.MANUFACTURER, Build.MODEL, Build.VERSION.RELEASE) + return field + } + private set + private const val MB = (1024 * 1024).toLong() + private var sLevelCache: LEVEL? = null + private var sTotalMemory: Long = 0 + + /** + * 获取设备等级,考虑到机身真实内存大小与参数规格里面的不太一致,排除系统可能占用的内存情况后,以下面方式来衡量: + * 1.内存小于等于4GB,定为中低端机 + * 2.内存大于4GB,定为高端机 + */ + fun getLevel(context: Context): LEVEL? { + if (null != sLevelCache) { + return sLevelCache + } + val totalMemory = getTotalMemory(context) + sLevelCache = if (totalMemory > 4 * 1024 * MB) { + //内存大于4GB,定为高端机 + LEVEL.HIGH + } else { + //内存小于等于4GB,定为中低端机 + LEVEL.LOW + } + return sLevelCache + } + + /** + * @return 当前进程id + */ + private val appId: Int + get() = Process.myPid() + + /** + * 获取设备内存总大小 + */ + private fun getTotalMemory(context: Context): Long { + if (0L != sTotalMemory) { + return sTotalMemory + } + val memInfo = ActivityManager.MemoryInfo() + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + am.getMemoryInfo(memInfo) + sTotalMemory = memInfo.totalMem + return sTotalMemory + } + + /** + * 当前设备是否处于低内存状态 + */ + fun isLowMemory(context: Context): Boolean { + val memInfo = ActivityManager.MemoryInfo() + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + am.getMemoryInfo(memInfo) + return memInfo.lowMemory + } + + /** + * 获取当前进程剩余内存大小 + */ + val appAvailableMemory: Long + get() { + val runtime = Runtime.getRuntime() + return runtime.totalMemory() - runtime.freeMemory() + } + + /** + * 获取当前设备CPU使用率 + */ + val appCpuRate: Double + get() { + var cpuTime = 0L + var appTime = 0L + var cpuRate = 0.0 + var procStatFile: RandomAccessFile? = null + var appStatFile: RandomAccessFile? = null + try { + procStatFile = RandomAccessFile("/proc/stat", "r") + val procStatString = procStatFile.readLine() + val procStats = procStatString.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + cpuTime = + procStats[2].toLong() + procStats[3].toLong() + procStats[4].toLong() + procStats[5].toLong() + procStats[6].toLong() + procStats[7].toLong() + procStats[8].toLong() + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + procStatFile?.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + try { + appStatFile = RandomAccessFile("/proc/$appId/stat", "r") + val appStatString = appStatFile.readLine() + val appStats = + appStatString.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + appTime = appStats[13].toLong() + appStats[14].toLong() + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + appStatFile?.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + if (0L != cpuTime) { + cpuRate = appTime.toDouble() / cpuTime.toDouble() * 100.0 + } + return cpuRate + } + + /** + * 获取设备id + */ + @JvmStatic + fun getDeviceId(context: Context?): String { + if (!TextUtils.isEmpty(deviceId)) { + return deviceId + } + deviceId = Config.getInstance(context).getString(cache_device_id, "") + if (!TextUtils.isEmpty(deviceId)) { + return deviceId + } + context?.let { + deviceId = getUUDeviceId(it) + } + if (TextUtils.isEmpty(deviceId)) { + deviceId = handyDevicesId + LibLogger.info(TAG, "use handy deviceId=$deviceId") + } else { + LibLogger.info(TAG, "use system deviceId=$deviceId") + } + //缓存上次取到的数据,避免有些机型每次打开APP都能拿到一个新的设备id + Config.getInstance(context).putString(cache_device_id, deviceId) + return deviceId + } + + /** + * @return 获取系统id + */ + @SuppressLint("HardwareIds") + @JvmStatic + fun getAndroidID(): String { + if (!TextUtils.isEmpty(ANDROID_ID)) { + LibLogger.info(TAG, "getAndroidID from cache, ANDROID_ID=$ANDROID_ID") + return ANDROID_ID + } + ANDROID_ID = SPUtils.getString(ANDROID_ID_KEY, "") + if (!TextUtils.isEmpty(ANDROID_ID)) { + LibLogger.info(TAG, "getAndroidID from SP, ANDROID_ID=$ANDROID_ID") + return ANDROID_ID + } + ANDROID_ID = Settings.Secure.getString( + BaseApp.getContext().contentResolver, + Settings.Secure.ANDROID_ID + ) + LibLogger.info(TAG, "getAndroidID from system, ANDROID_ID=$ANDROID_ID") + SPUtils.putString(ANDROID_ID_KEY, ANDROID_ID) + return ANDROID_ID + } + + /** + * @return 手动组装设备id + */ + private val handyDevicesId: String + get() { + val devIDShort = + "35" + Build.BOARD.length % 10 + Build.BRAND.length % 10 + Build.DEVICE.length % 10 + Build.MANUFACTURER.length % 10 + Build.MODEL.length % 10 + Build.PRODUCT.length % 10 + val serial = "serial" + return UUID(devIDShort.hashCode().toLong(), serial.hashCode().toLong()).toString() + } + + /** + * @return 移动设备国家代码(英语:Mobile Country Code,MCC)+移动设备网络代码(英语:Mobile Network Code,MNC) + */ + private fun getMCC_MNC(context: Context): String { + if (!TextUtils.isEmpty(mcc_mnc)) { + return mcc_mnc + } + try { + val telManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + mcc_mnc = telManager.simOperator + if (!TextUtils.isEmpty(mcc_mnc)) { + //双卡双待手机会返回[46001,46002] or [,46007] or [46007,]这种格式, + //先取第一个,如果第一个为空且第二个不为空,则取第二个 + val s = mcc_mnc.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (s.isNotEmpty() && !TextUtils.isEmpty(s[0])) { + mcc_mnc = s[0] + } else { + if (s.size == 2 && !TextUtils.isEmpty(s[1])) { + mcc_mnc = s[1] + } + } + } + return mcc_mnc + } catch (e: Exception) { + LibLogger.error(TAG, e.message) + } + return "" + } + + /** + * @return 移动设备国家代码(英语:Mobile Country Code,MCC),三位 + */ + fun getMCC(context: Context): String { + if (!TextUtils.isEmpty(mcc)) { + return mcc + } + try { + val mcc_mnc = getMCC_MNC(context) + if (mcc_mnc.length > 2) { + mcc = mcc_mnc.substring(0, 3) + return mcc + } + } catch (e: Exception) { + LibLogger.error(TAG, e.message) + } + return "" + } + + /** + * @return 移动设备网络代码(英语:Mobile Network Code,MNC),两位或三位 + */ + fun getMNC(context: Context): String { + if (!TextUtils.isEmpty(mnc)) { + return mnc + } + try { + val mcc_mnc = getMCC_MNC(context) + if (mcc_mnc.length > 3) { + mnc = mcc_mnc.substring(3) + return mnc + } + } catch (e: Exception) { + LibLogger.error(TAG, e.message) + } + return "" + } + + /** + * 检查指定包名的APP是否已安装了 + * + * @param context 上下文对象 + * @param packageName 包名 + * @return 是否有安装指定包名的APP,true代表已安装,false代表未安装 + */ + fun isAppInstalled(context: Context?, packageName: String): Boolean { + return if (context == null) { + false + } else { + var installed = false + try { + val pm = context.packageManager + val info = pm.getApplicationInfo(packageName, 0) + if (info != null) { + installed = true + } + } catch (e: Exception) { + LibLogger.error(TAG, "isAppInstalled packageName=$packageName", e) + } + installed + } + } + + /** + * 设备等级划分方案参考Matrix框架, + * 参考:[...](https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/DeviceUtil.java) + */ + enum class LEVEL(var value: Int) { + HIGH(2), LOW(1); + + } + + + private var UU_DEVICE_ID by mmkv.notNullString() + @JvmStatic + @SuppressLint("HardwareIds") + fun getUUDeviceId(context: Context): String { + synchronized(DeviceUtil::class.java) { + if (UU_DEVICE_ID.isEmpty()) { + val androidId = + Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + val uuid = if ("9774d56d682e549c" != androidId) { + UUID.nameUUIDFromBytes(androidId.toByteArray(StandardCharsets.UTF_8)) + } else { + UUID.randomUUID() + } + UU_DEVICE_ID = uuid.toString() + } + } + return UU_DEVICE_ID + } +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/DoubleUtils.java b/library/src/module_common/java/com/chwl/library/common/util/DoubleUtils.java new file mode 100644 index 0000000..f4904c2 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/DoubleUtils.java @@ -0,0 +1,103 @@ +package com.chwl.library.common.util; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精 确的浮点数运算,包括加减乘除和四舍五入。 + */ +public class DoubleUtils{ + private static final int DEF_DIV_SCALE = 2; + + /** + * @Description 两个Double数相加 + * + * @param d1 + * @param d2 + * @return Double + */ + public static Double add(Double d1,Double d2){ + BigDecimal b1 = new BigDecimal(d1.toString()); + BigDecimal b2 = new BigDecimal(d2.toString()); + return b1.add(b2).doubleValue(); + } + + /** + * @Description 两个Double数相减 + * + * @param d1 + * @param d2 + * @return Double + */ + public static Double sub(Double d1,Double d2){ + BigDecimal b1 = new BigDecimal(d1.toString()); + BigDecimal b2 = new BigDecimal(d2.toString()); + return b1.subtract(b2).doubleValue(); + } + + /** + * @Description 两个Double数相乘 + * + * @param d1 + * @param d2 + * @return Double + */ + public static Double mul(Double d1,Double d2){ + BigDecimal b1 = new BigDecimal(d1.toString()); + BigDecimal b2 = new BigDecimal(d2.toString()); + return b1.multiply(b2).doubleValue(); + } + + /** + * @Description 两个Double数相除 + * + * @param d1 + * @param d2 + * @return Double + */ + public static Double div(Double d1,Double d2){ + BigDecimal b1 = new BigDecimal(d1.toString()); + BigDecimal b2 = new BigDecimal(d2.toString()); + return b1.divide(b2,DEF_DIV_SCALE,BigDecimal.ROUND_HALF_UP).doubleValue(); + } + + /** + * @Description 两个Double数相除,并保留scale位小数 + * + * @param d1 + * @param d2 + * @param scale + * @return Double + */ + public static Double div(Double d1,Double d2,int scale){ + if(scale<0){ + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(d1.toString()); + BigDecimal b2 = new BigDecimal(d2.toString()); + return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue(); + } + + /** + * @Description String类型小数与Double类型的转换 + */ + public static void StrToDouble(){ + String str="1234.5678"; + double num; + DecimalFormat myformat = new DecimalFormat("#0.00"); + myformat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + num = Double.parseDouble(str);//直接转换为double类型 + num = Double.parseDouble(myformat.format(num));//保留2为小数 + System.out.println(num); + } + + public static String convertDoubleToString(double number) { + DecimalFormat df = new DecimalFormat("######0"); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return df.format(number); + } + +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/ExecutorCenter.java b/library/src/module_common/java/com/chwl/library/common/util/ExecutorCenter.java new file mode 100644 index 0000000..59aa5a9 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ExecutorCenter.java @@ -0,0 +1,57 @@ +package com.chwl.library.common.util; + +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +public class ExecutorCenter { + private static final String TAG = "ExecutorCenter"; + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static ExecutorCenter msInstance = null; + private static ScheduledExecutor mExecutor; + + public static ExecutorCenter getInstance() { + if (null != msInstance) return msInstance; + synchronized (ExecutorCenter.class) { + if (null != msInstance) return msInstance; + msInstance = new ExecutorCenter(); + return msInstance; + } + } + + private ExecutorCenter() { + mExecutor = Pools.newScheduledThreadPoolExecutor("self-executor", CPU_COUNT + 1); + } + + public Executor getExecutor() { + return mExecutor; + } + + public void post(Runnable runnable) { + if (this.checkNull(runnable)) { + return; + } + mExecutor.execute(runnable); + } + + public void postDelay(Runnable runnable, long delay) { + if (this.checkNull(runnable)) { + return; + } + mExecutor.execute(runnable, delay); + } + + public Future submitDelay(Runnable runnable, long delay) { + if (this.checkNull(runnable)) { + return null; + } + return mExecutor.submit(runnable, delay); + } + + private boolean checkNull(Runnable runnable) { + if (null == runnable) { + LibLogger.error(TAG, "runnable null!!!!"); + return true; + } + return false; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/LibLogger.java b/library/src/module_common/java/com/chwl/library/common/util/LibLogger.java new file mode 100644 index 0000000..bd6c57c --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/LibLogger.java @@ -0,0 +1,719 @@ +/** + * Log类。可以直接使用静态函数 + * 也可以用某个tag生成一个logger对象 + * 使用前需要先调用init初始化 + * 内部使用android的Log类实现,并支持写入文件 + */ +package com.chwl.library.common.util; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.chwl.library.utils.StringUtils; +import com.chwl.library.utils.config.BasicConfig; +import com.chwl.library.utils.log.MLog; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * @author daixiang + * + */ +@SuppressLint("SimpleDateFormat") +public class LibLogger { + + public enum LogLevel { + Verbose, + Debug, + Info, + Warn, + Error + } + + // 写log文件策略 + public enum LogFilePolicy { + NoLogFile, // 不写文件 + PerDay, // 一天只产生一个log文件 + PerLaunch // 每次运行均产生一个log文件 + } + + public static class LogConfig { + public String dir; // log文件目录,绝对路径 + public LogFilePolicy policy; + public LogLevel outputLevel; // 输出级别,大于等于此级别的log才会输出 + public LogLevel fileLevel; // 输出到文件的级别,大于等于此级别的log才会写入文件 + public int fileFlushCount; // 每次累计log超过此条数时,会检查是否需要flush log文件 + public int fileFlushInterval; // 定时每隔一定秒数检查是否需要flush log文件 + public int fileFlushMinInterval; // 距离上次flush最少需要多少秒 + + public LogConfig() { + policy = LogFilePolicy.PerLaunch; + outputLevel = LogLevel.Verbose; + fileLevel = LogLevel.Info; + fileFlushCount = 10; + fileFlushInterval = 60; + fileFlushMinInterval = 10; + } + public LogConfig(LogConfig cfg) { + this.dir = cfg.dir; + this.policy = cfg.policy; + this.outputLevel = cfg.outputLevel; + this.fileLevel = cfg.fileLevel; + this.fileFlushCount = cfg.fileFlushCount; + this.fileFlushInterval = cfg.fileFlushInterval; + this.fileFlushMinInterval = cfg.fileFlushMinInterval; + } + } + + + private static ConcurrentHashMap loggers = new ConcurrentHashMap(); +// private static Context context; + private static LoggerThread loggerThread; // 用于在另一个线程写log文件 + + private static LogConfig config = new LogConfig(); + // 写文件线程未准备好的时候,将可以写入文件的log先缓存起来 + private static List logList = Collections.synchronizedList(new ArrayList()); + + private String myTag; + + private LibLogger(String tag) { + myTag = tag; + } + + public String getTag() { + return myTag; + } + +// public static void init(Context ctx) { +// +// LogConfig cfg = new LogConfig(); +// if (ctx != null) { +// File f = ctx.getExternalCacheDir(); +// if (f != null +// && Environment.getExternalStorageState().equals( +// Environment.MEDIA_MOUNTED)) { +// Log.i("Logger", "cache dir = " + f.getAbsolutePath()); +// cfg.dir = f.getAbsolutePath() + "/logs"; +// } else { +// Log.i("Logger", "no extenal storage available"); +// f = ctx.getCacheDir(); +// if (f != null) { +// cfg.dir = f.getAbsolutePath() + "/logs"; +// } +// } +// } +// cfg.policy = LogFilePolicy.PerLaunch; +// cfg.outputLevel = LogLevel.Verbose; +// cfg.fileLevel = LogLevel.Info; +// +// Logger.init(ctx, cfg); +// } + + /** + * 使用Logger之前,必须先init + * @param cfg + */ + public static void init(LogConfig cfg) { + +// context = ctx; + info("Logger", "init Logger"); + config = new LogConfig(cfg); + +// if (config.policy != LogFilePolicy.NoLogFile && loggerThread == null) { +// loggerThread = new LoggerThread("LoggerThread", config); +// loggerThread.start(); +// } + + initMLog(config); + } + + public static void initMLog(LogConfig cfg) { + if (cfg.policy != LogFilePolicy.NoLogFile) { + String logDir = cfg.dir; + MLog.LogOptions options = new MLog.LogOptions(); + if (BasicConfig.INSTANCE.isDebuggable()) { + options.logLevel = MLog.LogOptions.LEVEL_VERBOSE; + } else { + options.logLevel = MLog.LogOptions.LEVEL_INFO; + } + options.honorVerbose = false; + options.logFileName = "logs.txt"; + MLog.initialize(logDir, options); + MLog.info("Logger", "init MLog, logFilePath = " + logDir + File.separator + options.logFileName); + } + } + + public static LibLogger getLogger(String tag) { + if (StringUtils.isEmpty(tag)) { + tag = "Default"; + } + LibLogger libLogger; + try { + libLogger = loggers.get(tag); + if (libLogger == null) { + libLogger = new LibLogger(tag); + loggers.put(tag, libLogger); + } + } catch (Exception e) { + MLog.error("Logger", "getLogger error! " + e); + libLogger = new LibLogger(tag); + } + + return libLogger; + } + + public static LibLogger getLogger(Class cls) { + if (cls == null) { + return LibLogger.getLogger(""); + } + +// String className = cls.getName(); +// String tag = className.substring(className.lastIndexOf(".") + 1); + return LibLogger.getLogger(cls.getSimpleName()); + } + + private static boolean isLoggable(LogLevel level) { + return level.compareTo(config.outputLevel) >= 0; + } + + private static String levelToString(LogLevel level) { + String str = ""; + switch (level) { + case Debug: + str = "Debug"; + break; + case Error: + str = "Error"; + break; + case Info: + str = "Info"; + break; + case Verbose: + str = "Verbose"; + break; + case Warn: + str = "Warn"; + break; + default: + str = "Debug"; + break; + } + return str; + } + + public static String getLogFilePath() { + if (loggerThread != null) { + return loggerThread.getFilePath(); + } else { + return null; + } + } + + private static void logToFile(String tag, LogLevel level, String message, Throwable t) { + + if (config.policy != LogFilePolicy.NoLogFile) { + if (loggerThread == null || !loggerThread.isReady()) { + // 文件线程未准备好,先缓存 + logList.add(LoggerThread.getFormattedString(tag, level, message)); + } else { + loggerThread.logToFile(tag, level, message, t); + } + } + } + + public static void log(String tag, LogLevel level, String message) { + if (LibLogger.isLoggable(level)) { + message = msgForTextLog(tag, message); + switch (level) { + case Debug: +// Log.d(tag, message); + MLog.debugWithoutLineNumber(tag, message); + break; + case Error: +// Log.e(tag, message); + MLog.errorWithoutLineNumber(tag, message); + break; + case Info: +// Log.i(tag, message); + MLog.infoWithoutLineNumber(tag, message); + break; + case Verbose: +// Log.v(tag, message); + MLog.verboseWithoutLineNumber(tag, message); + break; + case Warn: +// Log.w(tag, message); + MLog.warnWithoutLineNumber(tag, message); + break; + default: +// Log.d(tag, message); + MLog.debugWithoutLineNumber(tag, message); + break; + } +// logToFile(tag, level, message, null); + } + } + + private static void logError(String tag, String msg, Throwable tr) { + if (LibLogger.isLoggable(LogLevel.Error)) { +// msg = msgForTextLog(tag, msg); + if (tr == null) { +// Log.e(tag, msg); + MLog.error(tag, msg); + } else { +// Log.e(tag, msg, tr); + MLog.error(tag, msg, tr); + } +// logToFile(tag, LogLevel.Error, msg, tr); + } + } + +// public static void log(String tag, LogLevel level, String message, Throwable throwable) { +// switch (level) { +// case Debug: +// Log.d(tag, message, throwable); +// break; +// case Error: +// Log.e(tag, message, throwable); +// break; +// case Info: +// Log.i(tag, message, throwable); +// break; +// case Verbose: +// Log.v(tag, message, throwable); +// break; +// case Warn: +// Log.v(tag, message, throwable); +// break; +// default: +// Log.d(tag, message, throwable); +// break; +// } +// } + + private static String msgForTextLog(String tag, String message) { + if (message == null) { + message = "null"; + } + int line = -1; + String filename = null; + if (Thread.currentThread().getStackTrace().length > 4) { + line = Thread.currentThread().getStackTrace()[4].getLineNumber(); + filename = Thread.currentThread().getStackTrace()[4].getFileName(); + } + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(tag); + sb.append("] "); + sb.append(message); + sb.append("(P:"); + sb.append(android.os.Process.myPid()); + sb.append(")"); + sb.append("(T:"); + if (Looper.getMainLooper() == Looper.myLooper()) + sb.append("Main&"); + else + sb.append(Thread.currentThread().getId()); + sb.append(")"); +// sb.append("(C:"); +// sb.append(tag); +// sb.append(")"); + if (filename != null) { + sb.append(" at ("); + sb.append(filename); + } + if (line > 0) { + sb.append(":"); + sb.append(line); + sb.append(")"); + } + return sb.toString(); + } + + public static void verbose(String tag, String message) { +// message = msgForTextLog(tag, message); + LibLogger.log(tag, LogLevel.Verbose, message); + } + + public static void debug(String tag, String message) { +// message = msgForTextLog(tag, message); + LibLogger.log(tag, LogLevel.Debug, message); + } + + public static void info(String tag, String message) { +// message = msgForTextLog(tag, message); + LibLogger.log(tag, LogLevel.Info, message); + } + + public static void warn(String tag, String message) { +// message = msgForTextLog(tag, message); + LibLogger.log(tag, LogLevel.Warn, message); + } + + public static void error(String tag, String message) { +// message = msgForTextLog(tag, message); + LibLogger.log(tag, LogLevel.Error, message); + } + + public static void error(String tag, String message, Throwable throwable) { +// message = msgForTextLog(tag, message); + LibLogger.logError(tag, message, throwable); + } + + public void verbose(String message) { + LibLogger.verbose(myTag, message); +// message = msgForTextLog(myTag, message); +// MLog.verboseWithoutLineNumber(myTag, message); + } + + public void debug(String message) { + LibLogger.debug(myTag, message); +// message = msgForTextLog(myTag, message); +// MLog.debugWithoutLineNumber(myTag, message); + } + + public void info(String message) { + LibLogger.info(myTag, message); +// message = msgForTextLog(myTag, message); +// MLog.infoWithoutLineNumber(myTag, message); + } + + public void warn(String message) { + LibLogger.warn(myTag, message); +// message = msgForTextLog(myTag, message); +// MLog.warnWithoutLineNumber(myTag, message); + } + + public void error(String message) { + LibLogger.error(myTag, message); +// message = msgForTextLog(myTag, message); +// MLog.errorWithoutLineNumber(myTag, message); + } + + public void error(String message, Throwable throwable) { + LibLogger.logError(myTag, message, throwable); +// message = msgForTextLog(myTag, message); +// MLog.errorWithoutLineNumber(myTag, message, throwable); + } + + public static void onTerminate() { + if (loggerThread != null) { + loggerThread.sendFlush(); + } + } + +// private static class SdkLogger implements ILog { +// +// @Override +// public void verbose(String tag, String msg) { +// +// Logger.verbose(tag, msg); +// } +// +// @Override +// public void debug(String tag, String msg) { +// +// Logger.debug(tag, msg); +// } +// +// @Override +// public void info(String tag, String msg) { +// +// Logger.info(tag, msg); +// } +// +// @Override +// public void warn(String tag, String msg) { +// +// Logger.warn(tag, msg); +// } +// +// @Override +// public void error(String tag, String msg) { +// +// Logger.error(tag, msg); +// } +// +// @Override +// public void error(String tag, String msg, Throwable t) { +// Logger.error(tag, msg, t); +// } +// +// } + + /** + * 用于写log文件的线程 + * @author daixiang + * + */ + private static class LoggerThread extends Thread { + + private static final int LogMessageType = 0; + private static final int TimerMessageType = 1; + private static final int LogThrowableType = 2; + private static final int FlushLog = 3; + + private LogThreadHandler handler; // 使用此handler将log消息发到此线程处理 + private LogConfig config; + private String filePath; + private boolean isReady = false; + + public LoggerThread(String name, LogConfig cfg) { + super(name); + config = cfg; + } + + public boolean isReady() { + return isReady; + } + + private static String getFormattedString(String tag, LogLevel level, String msg) { + + String thread = (Looper.getMainLooper() == Looper.myLooper()) ? "[Main]" + : ("[" + Thread.currentThread().getId() + "]"); + String strLevel = "[" + LibLogger.levelToString(level) + "]"; + String logMsg = thread + "[" + tag + "]" + strLevel + " " + msg; + return logMsg; + } + + public void logToFile(String tag, LogLevel level, String msg, Throwable t) { + if ((config.policy != LogFilePolicy.NoLogFile) + && (level.compareTo(config.fileLevel) >= 0) + && (handler != null)) { + + String logMsg = getFormattedString(tag, level, msg); + + Message threadMessage = null; + if (t == null) { + threadMessage = handler.obtainMessage(LogMessageType); + threadMessage.obj = logMsg; + } else { + threadMessage = handler.obtainMessage(LogThrowableType); + threadMessage.obj = logMsg; + Bundle b = new Bundle(); + b.putSerializable("throwable", t); + threadMessage.setData(b); + } + + if (threadMessage != null) { + handler.sendMessage(threadMessage); + } + } + } + + public void sendFlush() { + if (handler != null) { + handler.sendEmptyMessage(FlushLog); + } + } + +// public void logToFile(String tag, LogLevel level, String msg) { +// logToFile(tag, level, msg, null); +// } + + public String getFilePath() { + return filePath; + } + + public void run() { + + Looper.prepare(); + + File logDir = new File(config.dir); + if (!logDir.exists()) { + LibLogger.info("Logger", "create log dir: " + logDir.getAbsolutePath()); + logDir.mkdirs(); + } + + SimpleDateFormat f; + if (config.policy == LogFilePolicy.PerLaunch) { + f = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-SSS"); + } else { + f = new SimpleDateFormat("yyyy-MM-dd"); + } + + filePath = config.dir + "/" + f.format(new Date()) + ".log"; + LibLogger.info("Logger", "log file name: " + filePath); + + handler = new LogThreadHandler(this); + isReady = true; + + // 将之前缓存的log先写入文件 + List list = new ArrayList(logList); + try { + if (list.size() > 0) { + LibLogger.debug("Logger", "write logs before logger thread ready to file: " + list.size()); + for (String s : list) { + handler.writeLine(s); + } + handler.flush(true); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + logList.clear(); + list.clear(); + list = null; + + Looper.loop(); + } + + private static class LogThreadHandler extends Handler { + + private SimpleDateFormat dateFormat; + private BufferedWriter writer; + private LoggerThread loggerThread; + private int logCounter; + private long lastFlushTime; + + private void writeLine(String formattedStr) throws IOException { + if (writer != null) { + writer.write(dateFormat.format(new Date()) + " " + formattedStr); + writer.newLine(); + } + } + + public LogThreadHandler(LoggerThread thread) { + loggerThread = thread; + dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + boolean append; + if (loggerThread.config.policy == LogFilePolicy.PerLaunch) { + append = false; + + } else { + append = true; + } + + try { + FileWriter fw = new FileWriter(loggerThread.filePath, append); + writer = new BufferedWriter(fw); + if (loggerThread.config.policy == LogFilePolicy.PerDay) { + writer.newLine(); + } +// writer.write(dateFormat.format(new Date()) + " " + loggerThread.getFormattedString("Logger", loggerThread.config.fileLevel, "---------------------Log Begin---------------------")); +// writer.newLine(); + + // 在文件开头加入一个易于识别的行 + writeLine(getFormattedString("Logger", loggerThread.config.fileLevel, "---------------------Log Begin---------------------")); + flush(true); + } catch (IOException e) { + writer = null; + e.printStackTrace(); + } + + if (writer != null && loggerThread.config.fileFlushInterval > 0) { + long time = loggerThread.config.fileFlushInterval * 1000; + new Timer().schedule(new TimerTask() { + + @Override + public void run() { + Message msg = obtainMessage(TimerMessageType); + sendMessage(msg); + } + }, time, time); + } + } + + public void flush(boolean force) throws IOException { + if (writer != null) { + + long now = System.currentTimeMillis(); + // 不要太频繁flush,最低间隔 + if ((now - lastFlushTime) > (loggerThread.config.fileFlushMinInterval * 1000)) { + writer.flush(); + lastFlushTime = System.currentTimeMillis(); + logCounter = 0; + } else { + logCounter++; + } + } + + } + + public void flushIfNeeded() throws IOException { + if (logCounter > loggerThread.config.fileFlushCount) { + flush(false); + } else { + logCounter++; + } + } + + @Override + public void handleMessage(Message msg) { + + if (writer == null) { + return; + } + + try { + switch (msg.what) { + + case LogMessageType: + { +// String str = dateFormat.format(new Date()) + " " +// + msg.obj; +// writer.write(str); +// writer.newLine(); + writeLine((String)msg.obj); + flushIfNeeded(); + break; + } + case LogThrowableType: + { +// String str = dateFormat.format(new Date()) + " " +// + msg.obj; +// writer.write(str); +// writer.newLine(); + writeLine((String)msg.obj); + Bundle data = msg.getData(); + if (data != null) { + Throwable t = (Throwable) data + .getSerializable("throwable"); + if (t != null) { + PrintWriter pw = new PrintWriter(writer); + t.printStackTrace(pw); + //pw.close(); // 不能close,否则内部的bufferedwriter也会被close! + writer.newLine(); + flush(true); // 异常,立刻flush + } else { + flushIfNeeded(); + } + } else { + flushIfNeeded(); + } + break; + } + case TimerMessageType: + flush(false); + break; + case FlushLog: + flush(true); + break; + default: + break; + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/LimitClickUtils.java b/library/src/module_common/java/com/chwl/library/common/util/LimitClickUtils.java new file mode 100644 index 0000000..6d53fc6 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/LimitClickUtils.java @@ -0,0 +1,62 @@ +package com.chwl.library.common.util; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +/** + * 防快速点击工具类 + */ +public class LimitClickUtils { + private Map mClickMap = new HashMap<>(); + + public boolean check() { + return this.check(null); + } + + public boolean check(Object object) { + return this.checkForTime(object, OneClick.MIN_CLICK_DELAY_TIME); + } + + public boolean checkForTime(Object object, int limitTime) { + String flag; + if (object == null) { + flag = Thread.currentThread().getStackTrace()[2].getMethodName(); + } else { + flag = object.toString(); + } + + if (this.mClickMap.get(flag) == null) { + this.mClickMap.put(flag, new OneClick(limitTime)); + } + return this.mClickMap.get(flag).check(); + } + + public boolean checkForTime(int limitTime) { + return this.checkForTime(null, limitTime); + } + + public void destroy() { + this.mClickMap.clear(); + } + + private static class OneClick { + public static final int MIN_CLICK_DELAY_TIME = 1000; + private long mLastClickTime = 0L; + private int mLimitTime; + + public OneClick(int limitTime) { + this.mLimitTime = limitTime; + } + + public boolean check() { + long currentTime = Calendar.getInstance().getTimeInMillis(); + if (currentTime - this.mLastClickTime > (long) this.mLimitTime) { + this.mLastClickTime = currentTime; + return false; + } + return true; + } + } +} + diff --git a/library/src/module_common/java/com/chwl/library/common/util/MMKVExt.kt b/library/src/module_common/java/com/chwl/library/common/util/MMKVExt.kt new file mode 100644 index 0000000..324eef3 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/MMKVExt.kt @@ -0,0 +1,120 @@ +package com.chwl.library.common.util + +import android.os.Parcelable +import com.tencent.mmkv.MMKV +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * @Author Vance + * Date:2023/09/08 0008 17:53 + */ + +val mmkv by lazy { + MMKV.defaultMMKV() +} + +private inline fun MMKV.delegate( + key: String? = null, + defaultValue: T, + crossinline getter: MMKV.(String?, T) -> T?, + crossinline setter: MMKV.(String, T) -> Boolean +): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>): T = + getter(key ?: property.name, defaultValue) ?: defaultValue + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + setter(key ?: property.name, value) + } + } + +fun MMKV.boolean( + key: String? = null, + defaultValue: Boolean = false +): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode) + +fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode) + +fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode) + +fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode) + +fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode) + +fun MMKV.notNullString(key: String? = null): ReadWriteProperty = + string(key, "") + +fun MMKV.string(key: String? = null, defaultValue: String): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeString, MMKV::encode) + +fun MMKV.byteArray( + key: String? = null, + defaultValue: ByteArray +): ReadWriteProperty = + delegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode) + +fun MMKV.stringSet( + key: String? = null, + defaultValue: Set +): ReadWriteProperty> = + delegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode) + +private inline fun MMKV.nullableDelegate( + key: String? = null, + crossinline getter: MMKV.(String?, T?) -> T?, + crossinline setter: MMKV.(String, T?) -> Boolean +): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>): T? = + getter(key ?: property.name, null) + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) { + setter(key ?: property.name, value) + } + } + +fun MMKV.byteArray( + key: String? = null, +): ReadWriteProperty = + nullableDelegate(key, MMKV::decodeBytes, MMKV::encode) + +fun MMKV.string( + key: String? = null, +): ReadWriteProperty = + nullableDelegate(key, MMKV::decodeString, MMKV::encode) + +fun MMKV.stringSet( + key: String? = null, +): ReadWriteProperty?> = + nullableDelegate(key, MMKV::decodeStringSet, MMKV::encode) + +inline fun MMKV.parcelable( + key: String? = null, + defaultValue: T +): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>): T = + decodeParcelable(key ?: property.name, T::class.java, defaultValue)?:defaultValue + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + encode(key ?: property.name, value) + } + } + +inline fun MMKV.parcelable( + key: String? = null, +): ReadWriteProperty = + object : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>): T? = + decodeParcelable(key ?: property.name, T::class.java) + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) { + encode(key ?: property.name, value) + } + } \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/OtherExt.kt b/library/src/module_common/java/com/chwl/library/common/util/OtherExt.kt new file mode 100644 index 0000000..28160a3 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/OtherExt.kt @@ -0,0 +1,371 @@ +package com.chwl.library.common.util + +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.text.Spannable +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.EditText +import android.widget.TextView +import androidx.core.text.TextUtilsCompat +import androidx.core.text.clearSpans +import androidx.core.view.ViewCompat +import androidx.core.widget.TextViewCompat +import com.chwl.library.BuildConfig +import com.chwl.library.utils.FormatUtils +import com.chwl.library.utils.ResUtil +import com.chwl.library.utils.SizeUtils +import com.example.lib_utils.AppUtils +import com.example.lib_utils.UiUtils.isRtl +import com.example.lib_utils.ktx.getString +import com.hjq.toast.ToastUtils +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.Locale + + +fun View?.setMargin(start:Int?=null,top:Int?=null,end:Int?=null,bottom:Int?=null,isDP:Boolean = true) { + if (this?.layoutParams != null) { + val lp = this.layoutParams + if (lp != null && lp is MarginLayoutParams){ + if (start != null) { + lp.marginStart = if (isDP)SizeUtils.dp2px(this.context,start.toFloat()) else start + } + + if (top != null) { + lp.topMargin = if (isDP)SizeUtils.dp2px(this.context,top.toFloat()) else top + } + + if (end != null) { + lp.marginEnd = if (isDP)SizeUtils.dp2px(this.context,end.toFloat()) else end + } + + if (bottom != null) { + lp.bottomMargin = if (isDP)SizeUtils.dp2px(this.context,bottom.toFloat()) else bottom + } + this.layoutParams = lp + } + } + +} + +fun View?.setPaddingRtl(start:Int?=null,top:Int?=null,end:Int?=null,bottom:Int?=null,isDP:Boolean = true) { + if (this != null) { + val pStart = if (start == null) this.paddingLeft else if(isDP) start.toDP() else start; + val pTop = if (top == null) this.paddingTop else if(isDP) top.toDP() else top; + val pEnd = if (end == null) this.paddingRight else if(isDP) end.toDP() else end; + val pBottom = if (bottom == null) this.paddingBottom else if(isDP) bottom.toDP() else bottom; + + if (isRtl(this.context)) { + this.setPadding(pEnd,pTop,pStart,pBottom) + } else { + this.setPadding(pStart,pTop,pEnd,pBottom) + } + } +} + +fun EditText?.hasInput() : Boolean { + return this?.text?.toString()?.trim()?.isVerify()?:false +} + + +fun View.setViewWH(width:Int?=null, height:Int?=null,isDP:Boolean = true) { +// this.post { + val lp = this.layoutParams + if (lp != null && lp is ViewGroup.LayoutParams){ + if (width != null) { + lp.width = if (isDP)SizeUtils.dp2px(this.context,width.toFloat()) else width + } + if (height != null) { + lp.height = if (isDP) SizeUtils.dp2px(this.context,height.toFloat()) else height + } + this.layoutParams = lp + } +// } +} + +fun View.setVis(isVis:Boolean,isInVis:Boolean = false) { + if (isInVis) { + if (isVis) { + if (this.visibility != View.VISIBLE){ + this.visibility = View.VISIBLE + } + } else { + if (this.visibility != View.INVISIBLE){ + this.visibility = View.INVISIBLE + } + } + } else { + if (isVis) { + if (this.visibility != View.VISIBLE){ + this.visibility = View.VISIBLE + } + } else { + if (this.visibility != View.GONE){ + this.visibility = View.GONE + } + } + } +} + +fun View.setVisNoSafe(isVis:Boolean,isInVis:Boolean = false) { + if (isInVis) { + if (isVis) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.INVISIBLE + } + } else { + if (isVis) { + this.visibility = View.VISIBLE + } else { + this.visibility = View.GONE + } + } +} + + +fun View?.setRL() { + if (this == null) return + if (isRtl(context)){ + this.scaleX = -1f + } +} + +fun Int.toDP() : Int { + return SizeUtils.dp2px(AppUtils.getApp().applicationContext,this.toFloat()) +} + + +fun TextView.setString(strId:Int,vararg vars: Any) { + this.text = ResUtil.getString(strId,vars) +} + +fun TextView.setString(strId:Int) { + this.text = ResUtil.getString(strId) +} +fun TextView.setString(str:String) { + this.text = str +} + +fun TextView.setAutoSizeModel(minSize:Int?=null,maxSize:Int?=null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (minSize != null && maxSize != null) { + this.setAutoSizeTextTypeUniformWithConfiguration(minSize, maxSize, 1, TypedValue.COMPLEX_UNIT_SP) + } else { + this.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM) + } + } else { + if (minSize != null && maxSize != null) { + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(this,minSize, maxSize, 1, TypedValue.COMPLEX_UNIT_SP) + } else { + TextViewCompat.setAutoSizeTextTypeWithDefaults(this,TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) + } + } +} + + +fun String?.isVerify() : Boolean { + return this?.isBlank() == false +} +fun CharSequence?.isVerify() : Boolean { + return this?.isBlank() == false +} + +fun String.toColor() : Int { + return Color.parseColor(this) +} + +fun String?.isSvgaUrl() : Boolean { + if (this.isVerify()) { + return this?.endsWith(".svga") == true || this?.endsWith(".SVGA") == true + } + return false +} +fun String?.isMp4() : Boolean { + if (this.isVerify()) { + return this?.endsWith(".mp4") == true + } + return false +} + +fun String?.doToast() { + if (this.isVerify()) { + ToastUtils.show(this) + } +} +fun String?.doToastDeBug() { + if (BuildConfig.DEBUG) { + if (this.isVerify()) { + ToastUtils.show(this) + } + } +} +fun String?.doLog() { + if (BuildConfig.DEBUG) { + if (this.isVerify()) { + val stackTrace = Thread.currentThread().stackTrace + val line = stackTrace?.getOrNull(3)?.lineNumber?:"" + val fileName = stackTrace?.getOrNull(3)?.fileName?:"" + Log.println(Log.DEBUG, "doLog", "$this at ($fileName:$line)") + } + } +} + +fun String?.doLog(doLog:Boolean) { + if (doLog) { + this.doLog() + } +} + +fun String?.doLogW() { + if (BuildConfig.DEBUG) { + if (this.isVerify()) { + val stackTrace = Thread.currentThread().stackTrace + val line = stackTrace?.getOrNull(3)?.lineNumber?:"" + val fileName = stackTrace?.getOrNull(3)?.fileName?:"" + Log.println(Log.WARN, "doLog", "$this at ($fileName:$line)") + } + } +} +fun String?.doLogE() { + if (BuildConfig.DEBUG) { + if (this.isVerify()) { + val stackTrace = Thread.currentThread().stackTrace + val line = stackTrace?.getOrNull(3)?.lineNumber?:"" + val fileName = stackTrace?.getOrNull(3)?.fileName?:"" + Log.println(Log.ERROR, "doLog", "$this at ($fileName:$line)") + } + } +} + +fun Int.doToast() { + val string = this.getString() + if (string.isVerify()) { + ToastUtils.show(this) + } +} + +fun List?.isVerify() : Boolean{ + return this?.isEmpty() == false +} + +fun List?.isVerify(pos:Int,run:(data:D)->Unit = {}) : D? { + return if (this.isNullOrEmpty()) { + null + } else { + if (pos >= 0 && pos < this.size) { + val d = this[pos] + if (d != null) { + run(d) + } + d + } else { + null + } + } +} + +fun Int.isVerify(list : List?) : Boolean{ + if (list.isVerify()) { + if (list!!.getOrNull(this) != null) { + return true + } + } + return false +} + + +fun Double?.toNumString(byte:Int = 2) : String { + if (this == null) { + return "0" + } else { + val intNum = this.toInt() + val floatNum = this - intNum + if (floatNum == 0.0) { + return intNum.toString() + } else { + val format = String.format(Locale.ENGLISH,"%.${byte}f", this) + if (format.endsWith(".00")){ + return intNum.toString() + }else if (format.contains(".") && format.endsWith("0")){ + return format.removeSuffix("0") + }else { + return format + } + } + + } +} + +/** + * 处理 富文本内存泄漏 + */ +fun TextView?.clearSpans() { + if (this != null) { + val text: CharSequence = this.getText() + if (text is Spannable) { + text.clearSpans() + } + this.text = "" + this.movementMethod = null + } +} + +/** + * 语言环境是否 右到左 + */ +fun Context?.isRtl() : Boolean{ + if (this == null) return false + val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + this.resources.configuration.locales.get(0) + } else { + this.resources.configuration.locale + } + return TextUtilsCompat.getLayoutDirectionFromLocale(locale) == ViewCompat.LAYOUT_DIRECTION_RTL +} + + +fun HashMap<*, *>?.isVerify(): Boolean { + if (this == null) return false + return this.isNotEmpty() +} + +fun Long.delayStart( callBack: () -> Unit = {}) : Job{ + return GlobalScope.launch { + delay(this@delayStart) + callBack() + } +} + +fun View?.postSafe(run:()->Unit) { + this?.post { + if (this?.isAttachedToWindow == true) { + run() + } + } +} + +fun View?.postSafe(time:Long,run:()->Unit) { + this?.postDelayed({ + if (this?.isAttachedToWindow == true) { + run() + } + },time) +} + + +fun Double?.formatToString() : String{ + if (this == null) return "" +// val numberFormat = NumberFormat.getInstance() +// numberFormat.isGroupingUsed = false +// numberFormat.maximumFractionDigits = 10 +// numberFormat.minimumFractionDigits = 0 + return FormatUtils.formatBigDecimal(this) +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/PhotoCompressUtil.kt b/library/src/module_common/java/com/chwl/library/common/util/PhotoCompressUtil.kt new file mode 100644 index 0000000..b380af3 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/PhotoCompressUtil.kt @@ -0,0 +1,182 @@ +package com.chwl.library.common.util + +import android.content.Context +import android.os.Environment +import android.text.TextUtils +import android.util.Log +import com.chwl.library.luban.Luban +import com.chwl.library.common.file.FileHelper +import com.chwl.library.common.Constants +import com.chwl.library.common.application.BaseApp +import kotlinx.coroutines.* +import java.io.File + +object PhotoCompressUtil { + private const val TAG = "PhotoCompressUtil" + private const val TAG_IMAGE_COMPRESS = "default" + private val photoExtensions = arrayOf("jpg", "png", "jpeg", "bmp", "webp") + + private fun checkIsPhoto(path: String?): Boolean { + if (TextUtils.isEmpty(path)) { + return false + } + for (extension in photoExtensions) { + if (path!!.endsWith(extension, true)) { + return true + } + } + return false + } + + @JvmOverloads + @JvmStatic + fun compress(context: Context, imgList: MutableList, outPath: String?, callback: PhotosCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): Job? { + val notImgMap = HashMap() + val imgs = ArrayList() + imgList.forEachIndexed { index, s -> + if (checkIsPhoto(s)) { + imgs.add(s) + } else { + notImgMap[index] = s + } + } + if (notImgMap.size == imgList.size) { + //纯非图片 + (imgList as? ArrayList)?.let { callback?.onSuccess(it) } + return null + } else { + return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch { + val deferred = async(Dispatchers.IO) { + var list: MutableList? = null + try { + list = Luban.with(context) + .load(imgs) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + + list + } + val compressedFileList = deferred.await() + if (!isActive) return@launch + if (compressedFileList.isNullOrEmpty()) { + callback?.onFail(Throwable("compress fail")) + return@launch + } + val compressedList = compressedFileList.map { it.path } as? ArrayList + + try { + if (notImgMap.isNotEmpty()) { + notImgMap.forEach { + compressedList?.add(it.key, it.value) + } + } + compressedList?.let { callback?.onSuccess(it) } + } catch (e: Exception) { + callback?.onFail(Throwable("compress fail")) + } + } + } + + } + + @JvmOverloads + @JvmStatic + fun compress(context: Context, imgPath: String, outPath: String?, callback: PhotoCompressCallback?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH):Job? { + if (!checkIsPhoto(imgPath)) { + callback?.onSuccess(imgPath) + return null + } else { + return CoroutineScope(Dispatchers.Main + SupervisorJob()).launch { + val deferred = async(Dispatchers.IO) { + var list: MutableList? = null + try { + list = Luban.with(context) + .load(imgPath) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + + list + } + + val compressedFileList = deferred.await() + if (!isActive) return@launch + if (compressedFileList.isNullOrEmpty()) { + callback?.onFail(Throwable("compress fail")) + } else { + callback?.onSuccess(compressedFileList[0].path) + } + } + } + } + + /** + * 同步线程-压缩 + */ + @JvmOverloads + @JvmStatic + fun synCompress(imgPath: String, outPath: String?, leastCompressSize: Int = 200, focusAlpha: Boolean = false, maxSize: Int = Constants.UPLOAD_IMAGE_MAX_SIZE, mostCompressSize: Int = Constants.UPLOAD_IMAGE_MAX_FILE_LENGTH): String? { + return if (!checkIsPhoto(imgPath)) { + imgPath + } else { + var compressedFileList: MutableList? = null + try { + compressedFileList = Luban.with(BaseApp.getContext()) + .load(imgPath) + .ignoreBy(leastCompressSize) + .setTargetDir(outPath) + .setFocusAlpha(focusAlpha) + .setMaxSize(maxSize) + .setMostCompressSize(mostCompressSize) + .get() + } catch (e: Exception) { + Log.e(TAG, "compress error: $e") + } + if (compressedFileList.isNullOrEmpty()) { + "" + } else { + compressedFileList[0].path + } + } + } + + @JvmStatic + fun getCompressCachePath(tag: String = TAG_IMAGE_COMPRESS): String { + val path = getCompressLocationPath(tag) + FileHelper.ensureDirExists(path) + return path + } + + @JvmStatic + fun clearCompressCache(tag: String = TAG_IMAGE_COMPRESS) { + FileHelper.removeAllFile(getCompressLocationPath(tag)) + } + + private fun getCompressLocationPath(tag: String): String { + return "${FileHelper.getRootFilesDir(Environment.DIRECTORY_PICTURES).absolutePath}/compress/${tag}/" + } + +} + +interface PhotosCompressCallback { + fun onSuccess(compressedImgList: ArrayList) + fun onFail(e: Throwable) +} + +interface PhotoCompressCallback { + fun onSuccess(compressedImg: String) + fun onFail(e: Throwable) +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/Pools.java b/library/src/module_common/java/com/chwl/library/common/util/Pools.java new file mode 100644 index 0000000..e3eae33 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/Pools.java @@ -0,0 +1,36 @@ +package com.chwl.library.common.util; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class Pools { + private static final String TAG = "Pools"; + + public static ScheduledExecutor newScheduledThreadPoolExecutor(String threadNamePrefix, int coreCount) { + return new ScheduledExecutorAdapter(new ScheduledThreadPoolExecutor(coreCount, new DefaultThreadFactory(threadNamePrefix)) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t != null) { + LibLogger.error(TAG, String.valueOf(t)); + } + } + }); + } + + private static class DefaultThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + DefaultThreadFactory(String threadNamePrefix) { + this.namePrefix = threadNamePrefix + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, this.namePrefix + this.threadNumber.getAndIncrement()); + } + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/ReflectionUtils.java b/library/src/module_common/java/com/chwl/library/common/util/ReflectionUtils.java new file mode 100644 index 0000000..bcfe453 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ReflectionUtils.java @@ -0,0 +1,40 @@ +package com.chwl.library.common.util; + +import java.lang.reflect.Field; + +public class ReflectionUtils { + private static final String TAG = "ReflectionUtils"; + + public static Field getDeclaredField(Object object, String fieldName) { + Class clazz = object.getClass(); + while (clazz != Object.class) { + try { + return clazz.getDeclaredField(fieldName); + } catch (Exception e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + public static void setFieldValue(Object object, String fieldName, Object value) { + Field field = ReflectionUtils.getDeclaredField(object, fieldName); + try { + field.setAccessible(true); + field.set(object, value); + } catch (Exception e) { + LibLogger.error(TAG, "ReflectionUtils setFieldValue", e); + } + } + + public static Object getFieldValue(Object object, String fieldName) { + Field field = ReflectionUtils.getDeclaredField(object, fieldName); + try { + field.setAccessible(true); + return field.get(object); + } catch (Exception e) { + LibLogger.error(TAG, "ReflectionUtils getFieldValue", e); + return null; + } + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/SPUtils.java b/library/src/module_common/java/com/chwl/library/common/util/SPUtils.java new file mode 100644 index 0000000..5177181 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/SPUtils.java @@ -0,0 +1,107 @@ +package com.chwl.library.common.util; + +import android.os.Parcelable; + +import com.chwl.library.common.application.BaseApp; +import com.chwl.library.utils.TimeUtils; + +import java.util.Date; +import java.util.Set; + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: 封装底层com.tcloud.core.util.Config,方便使用 + */ +public class SPUtils { + + private static Config sConfig = Config.getInstance(BaseApp.getContext()); + + public static void putBytes(String key, byte[] bytes) { + sConfig.putBytes(key, bytes); + } + + public static void putInt(String key, int value) { + sConfig.putInt(key, value); + } + + public static void putLong(String key, long value) { + sConfig.putLong(key, value); + } + + public static void putFloat(String key, float value) { + sConfig.putFloat(key, value); + } + + public static void putDouble(String key, double value) { + sConfig.putDouble(key, value); + } + + public static void putBoolean(String key, boolean value) { + sConfig.putBoolean(key, value); + } + + public static void putString(String key, String value) { + sConfig.putString(key, value); + } + + public static void putStringSet(String key, Set value) { + sConfig.putStringSet(key, value); + } + + public static void putParcelable(String key, Parcelable value) { + sConfig.putParcelable(key, value); + } + + public static byte[] getBytes(String key, byte[] defaultValue) { + return sConfig.getBytes(key, defaultValue); + } + + public static int getInt(String key, int defaultValue) { + return sConfig.getInt(key, defaultValue); + } + + public static long getLong(String key, long defaultValue) { + return sConfig.getLong(key, defaultValue); + } + + public static float getFloat(String key, float defaultValue) { + return sConfig.getFloat(key, defaultValue); + } + + public static double getDouble(String key, double defaultValue) { + return sConfig.getDouble(key, defaultValue); + } + + public static boolean getBoolean(String key, boolean defaultValue) { + return sConfig.getBoolean(key, defaultValue); + } + + public static String getString(String key, String defaultValue) { + return sConfig.getString(key, defaultValue); + } + + public static Set getStringSet(String key, Set defaultValue) { + return sConfig.getStringSet(key, defaultValue); + } + + public static T getParcelable(String key, Class tClass, T defaultValue) { + return sConfig.getParcelable(key, tClass, defaultValue); + } + + public static void remove(String key) { + sConfig.remove(key); + } + + public static void clearAll() { + sConfig.clearAll(); + } + + public static String getSharedDataKey(String constants, long playerId) { + String toDayStr = TimeUtils.date2Str(new Date(), "yyyy-MM-dd"); + return getAccountKey(constants, playerId) + toDayStr; + } + public static String getAccountKey(String key, long playerId) { + return key + playerId ; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutor.java b/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutor.java new file mode 100644 index 0000000..eb1669f --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutor.java @@ -0,0 +1,11 @@ +package com.chwl.library.common.util; + +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +public interface ScheduledExecutor extends Executor { + void execute(Runnable runnable, long delay); + + Future submit(Runnable runnable, long delay); +} + diff --git a/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutorAdapter.java b/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutorAdapter.java new file mode 100644 index 0000000..1dddaff --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ScheduledExecutorAdapter.java @@ -0,0 +1,34 @@ +package com.chwl.library.common.util; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ScheduledExecutorAdapter implements ScheduledExecutor { + private ScheduledExecutorService mExecutor; + + public ScheduledExecutorAdapter(ScheduledExecutorService executor) { + if (null == executor) { + throw new NullPointerException("ScheduledThreadPoolExecutor may not be null"); + } + this.mExecutor = executor; + } + + @Override + public void execute(Runnable command, long delay) { + this.mExecutor.schedule(command, delay, TimeUnit.MILLISECONDS); + } + + @Override + public void execute(@NonNull Runnable command) { + this.mExecutor.execute(command); + } + + @Override + public Future submit(Runnable command, long delay) { + return this.mExecutor.schedule(command, delay, TimeUnit.MILLISECONDS); + } +} + diff --git a/library/src/module_common/java/com/chwl/library/common/util/SoftKeyboardStateHelper.java b/library/src/module_common/java/com/chwl/library/common/util/SoftKeyboardStateHelper.java new file mode 100644 index 0000000..338f057 --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/SoftKeyboardStateHelper.java @@ -0,0 +1,89 @@ +package com.chwl.library.common.util; + +import android.graphics.Rect; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.util.LinkedList; +import java.util.List; + +public class SoftKeyboardStateHelper implements ViewTreeObserver.OnGlobalLayoutListener { + + public interface SoftKeyboardStateListener { + void onSoftKeyboardOpened(int keyboardHeightInPx); + void onSoftKeyboardClosed(); + } + + private final List listeners = new LinkedList(); + private final View activityRootView; + private int lastSoftKeyboardHeightInPx; + private boolean isSoftKeyboardOpened; + + public SoftKeyboardStateHelper(View activityRootView) { + this(activityRootView, false); + } + + public SoftKeyboardStateHelper(View activityRootView, boolean isSoftKeyboardOpened) { + this.activityRootView = activityRootView; + this.isSoftKeyboardOpened = isSoftKeyboardOpened; + activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(this); + } + + @Override + public void onGlobalLayout() { + final Rect r = new Rect(); + //r will be populated with the coordinates of your view that area still visible. + activityRootView.getWindowVisibleDisplayFrame(r); + + final int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top); + if (!isSoftKeyboardOpened && heightDiff > 100) { // if more than 100 pixels, its probably a keyboard... + isSoftKeyboardOpened = true; + notifyOnSoftKeyboardOpened(heightDiff); + } else if (isSoftKeyboardOpened && heightDiff < 100) { + isSoftKeyboardOpened = false; + notifyOnSoftKeyboardClosed(); + } + } + + public void setIsSoftKeyboardOpened(boolean isSoftKeyboardOpened) { + this.isSoftKeyboardOpened = isSoftKeyboardOpened; + } + + public boolean isSoftKeyboardOpened() { + return isSoftKeyboardOpened; + } + + /** + * Default value is zero (0) + * @return last saved keyboard height in px + */ + public int getLastSoftKeyboardHeightInPx() { + return lastSoftKeyboardHeightInPx; + } + + public void addSoftKeyboardStateListener(SoftKeyboardStateListener listener) { + listeners.add(listener); + } + + public void removeSoftKeyboardStateListener(SoftKeyboardStateListener listener) { + listeners.remove(listener); + } + + private void notifyOnSoftKeyboardOpened(int keyboardHeightInPx) { + this.lastSoftKeyboardHeightInPx = keyboardHeightInPx; + + for (SoftKeyboardStateListener listener : listeners) { + if (listener != null) { + listener.onSoftKeyboardOpened(keyboardHeightInPx); + } + } + } + + private void notifyOnSoftKeyboardClosed() { + for (SoftKeyboardStateListener listener : listeners) { + if (listener != null) { + listener.onSoftKeyboardClosed(); + } + } + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/util/Utils.java b/library/src/module_common/java/com/chwl/library/common/util/Utils.java new file mode 100644 index 0000000..a558f8e --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/Utils.java @@ -0,0 +1,178 @@ +package com.chwl.library.common.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import androidx.fragment.app.FragmentManager; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: 转换帮助类 + */ +public class Utils { + + public static boolean notEmpty(List list) { + return !isEmpty(list); + } + + public static boolean isEmpty(List list) { + if (list == null || list.size() == 0) { + return true; + } + return false; + } + + public static int getNavigationBarHeight(Context context) { + int statusBarHeight = 0; + int resourceId = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); + if (resourceId != 0) { + resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + //根据资源ID获取响应的尺寸值 + statusBarHeight = context.getResources().getDimensionPixelSize(resourceId); + } + return statusBarHeight; + } + + + /** + * 判断底部navigator是否已经显示 + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean hasSoftKeys(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display d = windowManager.getDefaultDisplay(); + + + DisplayMetrics realDisplayMetrics = new DisplayMetrics(); + d.getRealMetrics(realDisplayMetrics); + + + int realHeight = realDisplayMetrics.heightPixels; + int realWidth = realDisplayMetrics.widthPixels; + + + DisplayMetrics displayMetrics = new DisplayMetrics(); + d.getMetrics(displayMetrics); + + + int displayHeight = displayMetrics.heightPixels; + int displayWidth = displayMetrics.widthPixels; + + + return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0; + } + + // 将px值转换为dip或dp值 + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + // 将dip或dp值转换为px值 + public static int dip2px(Context context, float dipValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } + + // 将px值转换为sp值 + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + // 将sp值转换为px值 + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + // 屏幕宽度(像素) + public static int getWindowWidth(Context context) { + DisplayMetrics dm = context.getApplicationContext().getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + // 屏幕高度(像素) + public static int getWindowHeight(Context context) { + DisplayMetrics dm = context.getApplicationContext().getResources().getDisplayMetrics(); + return dm.heightPixels; + } + + // 根据Unicode编码判断中文汉字和符号 + private static boolean isChinese(char c) { + Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); + if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS + || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B + || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS + || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) { + return true; + } + return false; + } + + // 判断中文汉字和符号 + public static boolean isChinese(String strName) { + char[] ch = strName.toCharArray(); + for (int i = 0; i < ch.length; i++) { + char c = ch[i]; + if (isChinese(c)) { + return true; + } + } + return false; + } + + public static int getScreenPxWidth(Context context) { + WindowManager wm = (WindowManager) context + .getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(outMetrics); + return outMetrics.widthPixels; + } + + public static int getScreenDpWidth(Context context) { + int pxWidth = getScreenPxWidth(context); + return (int) px2dip(context, pxWidth); + } + + public static int getScreenPxHeight(Context context) { + WindowManager wm = (WindowManager) context + .getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(outMetrics); + return outMetrics.heightPixels; + } + + public static int getScreenDpHeight(Context context) { + int pxHeight = getScreenPxHeight(context); + return (int) px2dip(context, pxHeight); + } + + public static void executePendingTransactionsSafely(String TAG, FragmentManager fragmentManager) { + if (fragmentManager == null) { + LibLogger.error(TAG, "executePendingTransactionsSafely fragmentManager == null"); + return; + } + try { + fragmentManager.executePendingTransactions(); + } catch (Exception e) { + LibLogger.error(TAG, String.valueOf(e)); + try { + Field mExecutingActions = fragmentManager.getClass().getDeclaredField("mExecutingActions"); + mExecutingActions.setAccessible(true); + mExecutingActions.set(fragmentManager, false); + } catch (Exception e1) { + LibLogger.error(TAG, "set field value fail", e1); + } + } + } + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/util/ViewBindingUtil.kt b/library/src/module_common/java/com/chwl/library/common/util/ViewBindingUtil.kt new file mode 100644 index 0000000..62296ed --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/util/ViewBindingUtil.kt @@ -0,0 +1,97 @@ +package com.chwl.library.common.util + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.ParameterizedType + +/** + * author: wushaocheng + * time: 2022/11/15 + * desc: viewBinding fragment、activity绑定布局 + * @see 参考 https://github.com/DylanCaiCoding/ViewBindingKTX/blob/master/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ViewBindingUtil.kt + */ +object ViewBindingUtil { + private const val TAG = "ViewBindingUtil" + + + @JvmStatic + fun inflateWithActivity( + genericOwner: Any, layoutInflater: LayoutInflater + ): VB? = withGenericBindingClass(genericOwner) { clazz -> + clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as? VB + } + + @JvmStatic + fun inflateWithFragment( + genericOwner: Any, + layoutInflater: LayoutInflater, + parent: ViewGroup?, + ): VB? = withGenericBindingClass(genericOwner) { clazz -> + clazz.getMethod( + "inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java + ).invoke(null, layoutInflater, parent, false) as? VB + } + + + @JvmStatic + fun inflateWithView( + genericOwner: Any, + layoutInflater: LayoutInflater, + parent: ViewGroup?, + isAttach:Boolean = true + ): VB? = withGenericBindingClass(genericOwner) { clazz -> + var vb = try { + clazz.getMethod( + "inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java + ).invoke(null, layoutInflater, parent, isAttach) as? VB + } catch (e: Exception) { + //merge 标签会找不到 + LibLogger.debug( + TAG, + "inflateWithView => maybe use merge: " + genericOwner.javaClass.simpleName + ", genericSuperclass: " + genericOwner.javaClass.genericSuperclass + ) + null + } + if (vb == null) { + //merge 标签的只有该方法 + vb = clazz.getMethod( + "inflate", LayoutInflater::class.java, ViewGroup::class.java + ).invoke(null, layoutInflater, parent) as? VB + } + return@withGenericBindingClass vb + } + + private fun withGenericBindingClass( + genericOwner: Any, block: (Class) -> VB? + ): VB? { + var genericSuperclass = genericOwner.javaClass.genericSuperclass + var superclass = genericOwner.javaClass.superclass + while (superclass != null) { + if (genericSuperclass is ParameterizedType) { + genericSuperclass.actualTypeArguments.forEach { + try { + return block.invoke(it as Class) + } catch (e: NoSuchMethodException) { + } catch (e: ClassCastException) { + } catch (e: InvocationTargetException) { + LibLogger.error( + TAG, + "withGenericBindingClass => ${e.message}" + ", class: " + genericOwner.javaClass.simpleName + ) + return null + } + } + } + genericSuperclass = superclass.genericSuperclass + superclass = superclass.superclass + } + LibLogger.error( + TAG, + "withGenericBindingClass: " + genericOwner.javaClass.simpleName + ", genericSuperclass: " + genericOwner.javaClass.genericSuperclass + ) + return null + } + +} \ No newline at end of file diff --git a/library/src/module_common/java/com/chwl/library/common/widget/LinesFlexBoxLayoutManager.java b/library/src/module_common/java/com/chwl/library/common/widget/LinesFlexBoxLayoutManager.java new file mode 100644 index 0000000..9956cda --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/widget/LinesFlexBoxLayoutManager.java @@ -0,0 +1,56 @@ +package com.chwl.library.common.widget; + +import android.content.Context; + +import com.google.android.flexbox.FlexLine; +import com.google.android.flexbox.FlexboxLayoutManager; + +import java.util.List; + +/** + * author: wushaocheng + * time: 2022/2/17 + * desc: 一行显示FlexBoxManager + */ +public class LinesFlexBoxLayoutManager extends FlexboxLayoutManager { + + /** + * 最大行数 + * + * 小于等于0时,不限制行数 + */ + private int maxLines = 0; + + /** + * 设置最大显示行数 + * @param maxLines + */ + public void setMaxLines(int maxLines) { + this.maxLines = maxLines; + } + + public LinesFlexBoxLayoutManager(Context context) { + super(context); + } + + public LinesFlexBoxLayoutManager(Context context, int flexDirection) { + super(context, flexDirection); + } + + public LinesFlexBoxLayoutManager(Context context, int flexDirection, int flexWrap) { + super(context, flexDirection, flexWrap); + } + + /** + * 删掉多余的行数据 + */ + @Override + public List getFlexLinesInternal() { + List flexLines = super.getFlexLinesInternal(); + int size = flexLines.size(); + if (maxLines > 0 && size > maxLines) { + flexLines.subList(maxLines, size).clear(); + } + return flexLines; + } +} diff --git a/library/src/module_common/java/com/chwl/library/common/widget/VpRecyclerView.kt b/library/src/module_common/java/com/chwl/library/common/widget/VpRecyclerView.kt new file mode 100644 index 0000000..5a8121e --- /dev/null +++ b/library/src/module_common/java/com/chwl/library/common/widget/VpRecyclerView.kt @@ -0,0 +1,65 @@ +package com.chwl.library.common.widget + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.ViewConfiguration +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs + +/** + * author : wushaocheng + * date : 2023/2/3 + * desc : 滑动冲突。斜着往上滑的时候,会滑动viewpage + */ +class VpRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) { + private var mTouchSlop = 0 + + var startX = 0 //手指碰到屏幕时的 X坐标 + var startY = 0 //手机碰到屏幕时的 Y坐标 + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + when (ev?.action) { + MotionEvent.ACTION_DOWN -> { + //当手指按下时,得到了X,Y, + startX = ev.x.toInt() + startY = ev.y.toInt() + // + parent.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_MOVE -> { + //抬起手后得到的坐标, + val endX = ev.x.toInt() + val endY = ev.y.toInt() + //得到绝对值 。 + val disX = abs(endX - startX) + val disY = abs(endY - startY) + //如果X轴 大于Y 轴,说明实在左右移动 为什么? + // 屏幕坐标,X,Y从左上角开始。0,0 + if (disX > disY) { + Log.e("ACTIONdisX > disY:", "$disX") + //这个地方,判断了左右滑动的灵敏度,只有当左右滑动距离110 此时父布局才有作用,不拦截。 + if (disX > 110) { //结束的时候大于 + //当滑动的距离大于100的时候,才不拦截parent的事件 父控件才会有用。 + parent.requestDisallowInterceptTouchEvent(false) + } + } else { + // 说明是上下滑动 //canScrollVertically 检查此视图是否可以按某个方向垂直滚动。 负数表示上下滚动。正数表示左右滚动 + //return true如果视图可以按指定的方向滚动,否则为false。 + //既然是上下滑动,此时,父控件就不能有 事件 true停止 + parent.requestDisallowInterceptTouchEvent(true) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false) + } + return super.dispatchTouchEvent(ev) + } +} + + diff --git a/library/src/module_common/res/values-ar/strings.xml b/library/src/module_common/res/values-ar/strings.xml new file mode 100644 index 0000000..c96f9a6 --- /dev/null +++ b/library/src/module_common/res/values-ar/strings.xml @@ -0,0 +1,7 @@ + + فشل التحميل، الصورة كبيرة جدًا. + لا يمكن أن تكون الصورة المُرفوعة أصغر من 20 كيلوبايت. + الرجاء إدخال البيئة الصحيحة. + الرجاء إدخال البيئة الصحيحة. + الرجاء تهيئة البيئة أولاً. + diff --git a/library/src/module_common/res/values-zh-rTW/strings.xml b/library/src/module_common/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..695890b --- /dev/null +++ b/library/src/module_common/res/values-zh-rTW/strings.xml @@ -0,0 +1,7 @@ + + 上傳失敗,圖片太大啦~ + 上傳圖片不能小於20kb + 請輸入正確的環境 + 請輸入正確的環境 + 請先初始化環境 + diff --git a/library/src/module_common/res/values/ids.xml b/library/src/module_common/res/values/ids.xml new file mode 100644 index 0000000..9a0178f --- /dev/null +++ b/library/src/module_common/res/values/ids.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/library/src/module_common/res/values/strings.xml b/library/src/module_common/res/values/strings.xml new file mode 100644 index 0000000..ff2504b --- /dev/null +++ b/library/src/module_common/res/values/strings.xml @@ -0,0 +1,7 @@ + + Upload failed, image is too large. + Uploaded image cannot be smaller than 20kb. + Please enter the correct environment. + Please enter the correct environment. + Please initialize the environment first. + diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AfterPermissionGranted.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AfterPermissionGranted.java new file mode 100644 index 0000000..3a1c364 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AfterPermissionGranted.java @@ -0,0 +1,29 @@ +/* + * Copyright Google Inc. All Rights Reserved. + * + * 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.chwl.library.easypermisssion; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AfterPermissionGranted { + + int value(); + +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialog.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialog.java new file mode 100644 index 0000000..cae6c1f --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialog.java @@ -0,0 +1,358 @@ +package com.chwl.library.easypermisssion; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import com.chwl.library.R; + +/** + * Dialog to prompt the user to go to the app's settings screen and enable permissions. If the user + * clicks 'OK' on the dialog, they are sent to the settings screen. The result is returned to the + * Activity via {@see Activity#onActivityResult(int, int, Intent)}. + *

+ * Use the {@link Builder} to create and display a dialog. + */ +public class AppSettingsDialog implements Parcelable { + + private static final String TAG = "EasyPermissions"; + + public static final int DEFAULT_SETTINGS_REQ_CODE = 16061; + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final Creator CREATOR = new Creator() { + @Override + public AppSettingsDialog createFromParcel(Parcel in) { + return new AppSettingsDialog(in); + } + + @Override + public AppSettingsDialog[] newArray(int size) { + return new AppSettingsDialog[size]; + } + }; + + static final String EXTRA_APP_SETTINGS = "extra_app_settings"; + + @StyleRes + private final int mThemeResId; + private final String mRationale; + private final String mTitle; + private final String mPositiveButtonText; + private final String mNegativeButtonText; + private final int mRequestCode; + private final int mIntentFlags; + + private Object mActivityOrFragment; + private Context mContext; + + private AppSettingsDialog(Parcel in) { + mThemeResId = in.readInt(); + mRationale = in.readString(); + mTitle = in.readString(); + mPositiveButtonText = in.readString(); + mNegativeButtonText = in.readString(); + mRequestCode = in.readInt(); + mIntentFlags = in.readInt(); + } + + private AppSettingsDialog(@NonNull final Object activityOrFragment, + @StyleRes int themeResId, + @Nullable String rationale, + @Nullable String title, + @Nullable String positiveButtonText, + @Nullable String negativeButtonText, + int requestCode, + int intentFlags) { + setActivityOrFragment(activityOrFragment); + mThemeResId = themeResId; + mRationale = rationale; + mTitle = title; + mPositiveButtonText = positiveButtonText; + mNegativeButtonText = negativeButtonText; + mRequestCode = requestCode; + mIntentFlags = intentFlags; + } + + static AppSettingsDialog fromIntent(Intent intent, Activity activity) { + AppSettingsDialog dialog = intent.getParcelableExtra(AppSettingsDialog.EXTRA_APP_SETTINGS); + + // It's not clear how this could happen, but in the case that it does we should try + // to avoid a runtime crash and just use the default dialog. + // https://github.com/googlesamples/easypermissions/issues/278 + if (dialog == null) { + Log.e(TAG, "Intent contains null value for EXTRA_APP_SETTINGS: " + + "intent=" + intent + + ", " + + "extras=" + intent.getExtras()); + + dialog = new Builder(activity).build(); + } + + dialog.setActivityOrFragment(activity); + return dialog; + } + + private void setActivityOrFragment(Object activityOrFragment) { + mActivityOrFragment = activityOrFragment; + + if (activityOrFragment instanceof Activity) { + mContext = (Activity) activityOrFragment; + } else if (activityOrFragment instanceof Fragment) { + mContext = ((Fragment) activityOrFragment).getContext(); + } else { + throw new IllegalStateException("Unknown object: " + activityOrFragment); + } + } + + private void startForResult(Intent intent) { + if (mActivityOrFragment instanceof Activity) { + ((Activity) mActivityOrFragment).startActivityForResult(intent, mRequestCode); + } else if (mActivityOrFragment instanceof Fragment) { + ((Fragment) mActivityOrFragment).startActivityForResult(intent, mRequestCode); + } + } + + /** + * Display the built dialog. + */ + public void show() { + startForResult(AppSettingsDialogHolderActivity.createShowDialogIntent(mContext, this)); + } + + /** + * Show the dialog. {@link #show()} is a wrapper to ensure backwards compatibility + */ + AlertDialog showDialog(DialogInterface.OnClickListener positiveListener, + DialogInterface.OnClickListener negativeListener) { + AlertDialog.Builder builder; + if (mThemeResId != -1) { + builder = new AlertDialog.Builder(mContext, mThemeResId); + } else { + builder = new AlertDialog.Builder(mContext); + } + return builder + .setCancelable(false) + .setTitle(mTitle) + .setMessage(mRationale) + .setPositiveButton(mPositiveButtonText, positiveListener) + .setNegativeButton(mNegativeButtonText, negativeListener) + .show(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mThemeResId); + dest.writeString(mRationale); + dest.writeString(mTitle); + dest.writeString(mPositiveButtonText); + dest.writeString(mNegativeButtonText); + dest.writeInt(mRequestCode); + dest.writeInt(mIntentFlags); + } + + int getIntentFlags() { + return mIntentFlags; + } + + /** + * Builder for an {@link AppSettingsDialog}. + */ + public static class Builder { + + private final Object mActivityOrFragment; + private final Context mContext; + @StyleRes + private int mThemeResId = -1; + private String mRationale; + private String mTitle; + private String mPositiveButtonText; + private String mNegativeButtonText; + private int mRequestCode = -1; + private boolean mOpenInNewTask = false; + + /** + * Create a new Builder for an {@link AppSettingsDialog}. + * + * @param activity the {@link Activity} in which to display the dialog. + */ + public Builder(@NonNull Activity activity) { + mActivityOrFragment = activity; + mContext = activity; + } + + /** + * Create a new Builder for an {@link AppSettingsDialog}. + * + * @param fragment the {@link Fragment} in which to display the dialog. + */ + public Builder(@NonNull Fragment fragment) { + mActivityOrFragment = fragment; + mContext = fragment.getContext(); + } + + /** + * Set the dialog theme. + */ + @NonNull + public Builder setThemeResId(@StyleRes int themeResId) { + mThemeResId = themeResId; + return this; + } + + /** + * Set the title dialog. Default is "Permissions Required". + */ + @NonNull + public Builder setTitle(@Nullable String title) { + mTitle = title; + return this; + } + + /** + * Set the title dialog. Default is "Permissions Required". + */ + @NonNull + public Builder setTitle(@StringRes int title) { + mTitle = mContext.getString(title); + return this; + } + + /** + * Set the rationale dialog. Default is + * "This app may not work correctly without the requested permissions. + * Open the app settings screen to modify app permissions." + */ + @NonNull + public Builder setRationale(@Nullable String rationale) { + mRationale = rationale; + return this; + } + + /** + * Set the rationale dialog. Default is + * "This app may not work correctly without the requested permissions. + * Open the app settings screen to modify app permissions." + */ + @NonNull + public Builder setRationale(@StringRes int rationale) { + mRationale = mContext.getString(rationale); + return this; + } + + /** + * Set the positive button text, default is {@link android.R.string#ok}. + */ + @NonNull + public Builder setPositiveButton(@Nullable String text) { + mPositiveButtonText = text; + return this; + } + + /** + * Set the positive button text, default is {@link android.R.string#ok}. + */ + @NonNull + public Builder setPositiveButton(@StringRes int textId) { + mPositiveButtonText = mContext.getString(textId); + return this; + } + + /** + * Set the negative button text, default is {@link android.R.string#cancel}. + *

+ * To know if a user cancelled the request, check if your permissions were given with {@link + * EasyPermissions#hasPermissions(Context, String...)} in {@see + * Activity#onActivityResult(int, int, Intent)}. If you still don't have the right + * permissions, then the request was cancelled. + */ + @NonNull + public Builder setNegativeButton(@Nullable String text) { + mNegativeButtonText = text; + return this; + } + + /** + * Set the negative button text, default is {@link android.R.string#cancel}. + */ + @NonNull + public Builder setNegativeButton(@StringRes int textId) { + mNegativeButtonText = mContext.getString(textId); + return this; + } + + /** + * Set the request code use when launching the Settings screen for result, can be retrieved + * in the calling Activity's {@see Activity#onActivityResult(int, int, Intent)} method. + * Default is {@link #DEFAULT_SETTINGS_REQ_CODE}. + */ + @NonNull + public Builder setRequestCode(int requestCode) { + mRequestCode = requestCode; + return this; + } + + /** + * Set whether the settings screen should be opened in a separate task. This is achieved by + * setting {@link Intent#FLAG_ACTIVITY_NEW_TASK#FLAG_ACTIVITY_NEW_TASK} on + * the Intent used to open the settings screen. + */ + @NonNull + public Builder setOpenInNewTask(boolean openInNewTask) { + mOpenInNewTask = openInNewTask; + return this; + } + + /** + * Build the {@link AppSettingsDialog} from the specified options. Generally followed by a + * call to {@link AppSettingsDialog#show()}. + */ + @NonNull + public AppSettingsDialog build() { + mRationale = TextUtils.isEmpty(mRationale) ? + mContext.getString(R.string.rationale_ask_again) : mRationale; + mTitle = TextUtils.isEmpty(mTitle) ? + mContext.getString(R.string.title_settings_dialog) : mTitle; + mPositiveButtonText = TextUtils.isEmpty(mPositiveButtonText) ? + mContext.getString(android.R.string.ok) : mPositiveButtonText; + mNegativeButtonText = TextUtils.isEmpty(mNegativeButtonText) ? + mContext.getString(android.R.string.cancel) : mNegativeButtonText; + mRequestCode = mRequestCode > 0 ? mRequestCode : DEFAULT_SETTINGS_REQ_CODE; + + int intentFlags = 0; + if (mOpenInNewTask) { + intentFlags |= Intent.FLAG_ACTIVITY_NEW_TASK; + } + + return new AppSettingsDialog( + mActivityOrFragment, + mThemeResId, + mRationale, + mTitle, + mPositiveButtonText, + mNegativeButtonText, + mRequestCode, + intentFlags); + } + + } + +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialogHolderActivity.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialogHolderActivity.java new file mode 100644 index 0000000..5d6b7df --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/AppSettingsDialogHolderActivity.java @@ -0,0 +1,65 @@ +package com.chwl.library.easypermisssion; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import androidx.annotation.RestrictTo; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener { + private static final int APP_SETTINGS_RC = 7534; + + private AlertDialog mDialog; + private int mIntentFlags; + + public static Intent createShowDialogIntent(Context context, AppSettingsDialog dialog) { + Intent intent = new Intent(context, AppSettingsDialogHolderActivity.class); + intent.putExtra(AppSettingsDialog.EXTRA_APP_SETTINGS, dialog); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + AppSettingsDialog appSettingsDialog = AppSettingsDialog.fromIntent(getIntent(), this); + mIntentFlags = appSettingsDialog.getIntentFlags(); + mDialog = appSettingsDialog.showDialog(this, this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == Dialog.BUTTON_POSITIVE) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", getPackageName(), null)); + intent.addFlags(mIntentFlags); + startActivityForResult(intent, APP_SETTINGS_RC); + } else if (which == Dialog.BUTTON_NEGATIVE) { + setResult(Activity.RESULT_CANCELED); + finish(); + } else { + throw new IllegalStateException("Unknown button type: " + which); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + setResult(resultCode, data); + finish(); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/EasyPermissions.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/EasyPermissions.java new file mode 100644 index 0000000..7e18cf0 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/EasyPermissions.java @@ -0,0 +1,358 @@ +/* + * Copyright Google Inc. All Rights Reserved. + * + * 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.chwl.library.easypermisssion; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Size; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; +import androidx.core.content.ContextCompat; +import android.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import com.chwl.library.easypermisssion.helper.PermissionHelper; + +/** + * Utility to request and check System permissions for apps targeting Android M (API >= 23). + */ +public class EasyPermissions { + + /** + * Callback interface to receive the results of {@code EasyPermissions.requestPermissions()} + * calls. + */ + public interface PermissionCallbacks extends ActivityCompat.OnRequestPermissionsResultCallback { + + void onPermissionsGranted(int requestCode, @NonNull List perms); + + void onPermissionsDenied(int requestCode, @NonNull List perms); + } + + /** + * Callback interface to receive button clicked events of the rationale dialog + */ + public interface RationaleCallbacks { + void onRationaleAccepted(int requestCode); + + void onRationaleDenied(int requestCode); + } + + private static final String TAG = "EasyPermissions"; + + /** + * Check if the calling context has a set of permissions. + * + * @param context the calling context. + * @param perms one ore more permissions, such as {@link Manifest.permission#CAMERA}. + * @return true if all permissions are already granted, false if at least one permission is not + * yet granted. + * @see Manifest.permission + */ + public static boolean hasPermissions(@NonNull Context context, + @Size(min = 1) @NonNull String... perms) { + // Always return true for SDK < M, let the system deal with the permissions + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Log.w(TAG, "hasPermissions: API version < M, returning true by default"); + + // DANGER ZONE!!! Changing this will break the library. + return true; + } + + // Null context may be passed if we have detected Low API (less than M) so getting + // to this point with a null context should not be possible. + if (context == null) { + throw new IllegalArgumentException("Can't check permissions for null context"); + } + + for (String perm : perms) { + if (ContextCompat.checkSelfPermission(context, perm) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + + return true; + } + + /** + * Request a set of permissions, showing a rationale if the system requests it. + * + * @param host requesting context. + * @param rationale a message explaining why the application needs this set of permissions; + * will be displayed if the user rejects the request the first time. + * @param requestCode request code to track this request, must be < 256. + * @param perms a set of permissions to be requested. + * @see Manifest.permission + */ + public static void requestPermissions( + @NonNull Activity host, @NonNull String rationale, + @IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) { + requestPermissions( + new PermissionRequest.Builder(host, requestCode, perms) + .setRationale(rationale) + .build()); + } + + /** + * Request permissions from a Support Fragment with standard OK/Cancel buttons. + * + * @see #requestPermissions(Activity, String, int, String...) + */ + public static void requestPermissions( + @NonNull Fragment host, @NonNull String rationale, + @IntRange(from = 0, to = 255) int requestCode, @Size(min = 1) @NonNull String... perms) { + requestPermissions( + new PermissionRequest.Builder(host, requestCode, perms) + .setRationale(rationale) + .build()); + } + + /** + * Request a set of permissions. + * + * @param request the permission request + * @see PermissionRequest + */ + public static void requestPermissions(PermissionRequest request) { + + // Check for permissions before dispatching the request + if (hasPermissions(request.getHelper().getContext(), request.getPerms())) { + notifyAlreadyHasPermissions( + request.getHelper().getHost(), request.getRequestCode(), request.getPerms()); + return; + } + + // Request permissions + request.getHelper().requestPermissions( + request.getRationale(), + request.getPositiveButtonText(), + request.getNegativeButtonText(), + request.getTheme(), + request.getRequestCode(), + request.getPerms()); + } + + /** + * Handle the result of a permission request, should be called from the calling {@link + * Activity}'s {@link ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, + * String[], int[])} method. + *

+ * If any permissions were granted or denied, the {@code object} will receive the appropriate + * callbacks through {@link PermissionCallbacks} and methods annotated with {@link + * AfterPermissionGranted} will be run if appropriate. + * + * @param requestCode requestCode argument to permission result callback. + * @param permissions permissions argument to permission result callback. + * @param grantResults grantResults argument to permission result callback. + * @param receivers an array of objects that have a method annotated with {@link + * AfterPermissionGranted} or implement {@link PermissionCallbacks}. + */ + public static void onRequestPermissionsResult(@IntRange(from = 0, to = 255) int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults, + @NonNull Object... receivers) { + // Make a collection of granted and denied permissions from the request. + List granted = new ArrayList<>(); + List denied = new ArrayList<>(); + for (int i = 0; i < permissions.length; i++) { + String perm = permissions[i]; + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + granted.add(perm); + } else { + denied.add(perm); + } + } + + // iterate through all receivers + for (Object object : receivers) { + // Report granted permissions, if any. + if (!granted.isEmpty()) { + if (object instanceof PermissionCallbacks) { + ((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted); + } + } + + // Report denied permissions, if any. + if (!denied.isEmpty()) { + if (object instanceof PermissionCallbacks) { + ((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied); + } + } + + // If 100% successful, call annotated methods + if (!granted.isEmpty() && denied.isEmpty()) { + runAnnotatedMethods(object, requestCode); + } + } + } + + /** + * Check if at least one permission in the list of denied permissions has been permanently + * denied (user clicked "Never ask again"). + * + * Note: Due to a limitation in the information provided by the Android + * framework permissions API, this method only works after the permission + * has been denied and your app has received the onPermissionsDenied callback. + * Otherwise the library cannot distinguish permanent denial from the + * "not yet denied" case. + * + * @param host context requesting permissions. + * @param deniedPermissions list of denied permissions, usually from {@link + * PermissionCallbacks#onPermissionsDenied(int, List)} + * @return {@code true} if at least one permission in the list was permanently denied. + */ + public static boolean somePermissionPermanentlyDenied(@NonNull Activity host, + @NonNull List deniedPermissions) { + return PermissionHelper.newInstance(host) + .somePermissionPermanentlyDenied(deniedPermissions); + } + + /** + * @see #somePermissionPermanentlyDenied(Activity, List) + */ + public static boolean somePermissionPermanentlyDenied(@NonNull Fragment host, + @NonNull List deniedPermissions) { + return PermissionHelper.newInstance(host) + .somePermissionPermanentlyDenied(deniedPermissions); + } + + /** + * Check if a permission has been permanently denied (user clicked "Never ask again"). + * + * @param host context requesting permissions. + * @param deniedPermission denied permission. + * @return {@code true} if the permissions has been permanently denied. + */ + public static boolean permissionPermanentlyDenied(@NonNull Activity host, + @NonNull String deniedPermission) { + return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission); + } + + /** + * @see #permissionPermanentlyDenied(Activity, String) + */ + public static boolean permissionPermanentlyDenied(@NonNull Fragment host, + @NonNull String deniedPermission) { + return PermissionHelper.newInstance(host).permissionPermanentlyDenied(deniedPermission); + } + + /** + * See if some denied permission has been permanently denied. + * + * @param host requesting context. + * @param perms array of permissions. + * @return true if the user has previously denied any of the {@code perms} and we should show a + * rationale, false otherwise. + */ + public static boolean somePermissionDenied(@NonNull Activity host, + @NonNull String... perms) { + return PermissionHelper.newInstance(host).somePermissionDenied(perms); + } + + /** + * @see #somePermissionDenied(Activity, String...) + */ + public static boolean somePermissionDenied(@NonNull Fragment host, + @NonNull String... perms) { + return PermissionHelper.newInstance(host).somePermissionDenied(perms); + } + + /** + * Run permission callbacks on an object that requested permissions but already has them by + * simulating {@link PackageManager#PERMISSION_GRANTED}. + * + * @param object the object requesting permissions. + * @param requestCode the permission request code. + * @param perms a list of permissions requested. + */ + private static void notifyAlreadyHasPermissions(@NonNull Object object, + int requestCode, + @NonNull String[] perms) { + int[] grantResults = new int[perms.length]; + for (int i = 0; i < perms.length; i++) { + grantResults[i] = PackageManager.PERMISSION_GRANTED; + } + + onRequestPermissionsResult(requestCode, perms, grantResults, object); + } + + /** + * Find all methods annotated with {@link AfterPermissionGranted} on a given object with the + * correct requestCode argument. + * + * @param object the object with annotated methods. + * @param requestCode the requestCode passed to the annotation. + */ + private static void runAnnotatedMethods(@NonNull Object object, int requestCode) { + Class clazz = object.getClass(); + if (isUsingAndroidAnnotations(object)) { + clazz = clazz.getSuperclass(); + } + + while (clazz != null) { + for (Method method : clazz.getDeclaredMethods()) { + AfterPermissionGranted ann = method.getAnnotation(AfterPermissionGranted.class); + if (ann != null) { + // Check for annotated methods with matching request code. + if (ann.value() == requestCode) { + // Method must be void so that we can invoke it + if (method.getParameterTypes().length > 0) { + throw new RuntimeException( + "Cannot execute method " + method.getName() + " because it is non-void method and/or has input parameters."); + } + + try { + // Make method accessible if private + if (!method.isAccessible()) { + method.setAccessible(true); + } + method.invoke(object); + } catch (IllegalAccessException e) { + Log.e(TAG, "runDefaultMethod:IllegalAccessException", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "runDefaultMethod:InvocationTargetException", e); + } + } + } + } + + clazz = clazz.getSuperclass(); + } + } + + /** + * Determine if the project is using the AndroidAnnotations library. + */ + private static boolean isUsingAndroidAnnotations(@NonNull Object object) { + if (!object.getClass().getSimpleName().endsWith("_")) { + return false; + } + try { + Class clazz = Class.forName("org.androidannotations.api.view.HasViews"); + return clazz.isInstance(object); + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/PermissionRequest.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/PermissionRequest.java new file mode 100644 index 0000000..49ece5e --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/PermissionRequest.java @@ -0,0 +1,260 @@ +package com.chwl.library.easypermisssion; + +import android.app.Activity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.Size; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; + +import java.util.Arrays; + +import com.chwl.library.R; +import com.chwl.library.easypermisssion.helper.PermissionHelper; + +/** + * An immutable model object that holds all of the parameters associated with a permission request, + * such as the permissions, request code, and rationale. + * + * @see EasyPermissions#requestPermissions(PermissionRequest) + * @see Builder + */ +public final class PermissionRequest { + private final PermissionHelper mHelper; + private final String[] mPerms; + private final int mRequestCode; + private final String mRationale; + private final String mPositiveButtonText; + private final String mNegativeButtonText; + private final int mTheme; + + private PermissionRequest(PermissionHelper helper, + String[] perms, + int requestCode, + String rationale, + String positiveButtonText, + String negativeButtonText, + int theme) { + mHelper = helper; + mPerms = perms.clone(); + mRequestCode = requestCode; + mRationale = rationale; + mPositiveButtonText = positiveButtonText; + mNegativeButtonText = negativeButtonText; + mTheme = theme; + } + + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public PermissionHelper getHelper() { + return mHelper; + } + + @NonNull + public String[] getPerms() { + return mPerms.clone(); + } + + public int getRequestCode() { + return mRequestCode; + } + + @NonNull + public String getRationale() { + return mRationale; + } + + @NonNull + public String getPositiveButtonText() { + return mPositiveButtonText; + } + + @NonNull + public String getNegativeButtonText() { + return mNegativeButtonText; + } + + @StyleRes + public int getTheme() { + return mTheme; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PermissionRequest request = (PermissionRequest) o; + + return Arrays.equals(mPerms, request.mPerms) && mRequestCode == request.mRequestCode; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(mPerms); + result = 31 * result + mRequestCode; + return result; + } + + @Override + public String toString() { + return "PermissionRequest{" + + "mHelper=" + mHelper + + ", mPerms=" + Arrays.toString(mPerms) + + ", mRequestCode=" + mRequestCode + + ", mRationale='" + mRationale + '\'' + + ", mPositiveButtonText='" + mPositiveButtonText + '\'' + + ", mNegativeButtonText='" + mNegativeButtonText + '\'' + + ", mTheme=" + mTheme + + '}'; + } + + /** + * Builder to build a permission request with variable options. + * + * @see PermissionRequest + */ + public static final class Builder { + private final PermissionHelper mHelper; + private final int mRequestCode; + private final String[] mPerms; + + private String mRationale; + private String mPositiveButtonText; + private String mNegativeButtonText; + private int mTheme = -1; + + /** + * Construct a new permission request builder with a host, request code, and the requested + * permissions. + * + * @param activity the permission request host + * @param requestCode request code to track this request; must be < 256 + * @param perms the set of permissions to be requested + */ + public Builder(@NonNull Activity activity, int requestCode, + @NonNull @Size(min = 1) String... perms) { + mHelper = PermissionHelper.newInstance(activity); + mRequestCode = requestCode; + mPerms = perms; + } + + /** + * @see #Builder(Activity, int, String...) + */ + public Builder(@NonNull Fragment fragment, int requestCode, + @NonNull @Size(min = 1) String... perms) { + mHelper = PermissionHelper.newInstance(fragment); + mRequestCode = requestCode; + mPerms = perms; + } + + /** + * Set the rationale to display to the user if they don't allow your permissions on the + * first try. This rationale will be shown as long as the user has denied your permissions + * at least once, but has not yet permanently denied your permissions. Should the user + * permanently deny your permissions, use the {@link AppSettingsDialog} instead. + *

+ * + * @param rationale the rationale to be displayed to the user should they deny your + * permission at least once + */ + @NonNull + public Builder setRationale(@Nullable String rationale) { + mRationale = rationale; + return this; + } + + /** + * @param resId the string resource to be used as a rationale + * @see #setRationale(String) + */ + @NonNull + public Builder setRationale(@StringRes int resId) { + mRationale = mHelper.getContext().getString(resId); + return this; + } + + /** + * Set the positive button text for the rationale dialog should it be shown. + *

+ * The default is {@link android.R.string#ok} + */ + @NonNull + public Builder setPositiveButtonText(@Nullable String positiveButtonText) { + mPositiveButtonText = positiveButtonText; + return this; + } + + /** + * @see #setPositiveButtonText(String) + */ + @NonNull + public Builder setPositiveButtonText(@StringRes int resId) { + mPositiveButtonText = mHelper.getContext().getString(resId); + return this; + } + + /** + * Set the negative button text for the rationale dialog should it be shown. + *

+ * The default is {@link android.R.string#cancel} + */ + @NonNull + public Builder setNegativeButtonText(@Nullable String negativeButtonText) { + mNegativeButtonText = negativeButtonText; + return this; + } + + /** + * @see #setNegativeButtonText(String) + */ + @NonNull + public Builder setNegativeButtonText(@StringRes int resId) { + mNegativeButtonText = mHelper.getContext().getString(resId); + return this; + } + + /** + * Set the theme to be used for the rationale dialog should it be shown. + * + * @param theme a style resource + */ + @NonNull + public Builder setTheme(@StyleRes int theme) { + mTheme = theme; + return this; + } + + /** + * Build the permission request. + * + * @return the permission request + * @see EasyPermissions#requestPermissions(PermissionRequest) + * @see PermissionRequest + */ + @NonNull + public PermissionRequest build() { + if (mRationale == null) { + mRationale = mHelper.getContext().getString(R.string.rationale_ask); + } + if (mPositiveButtonText == null) { + mPositiveButtonText = mHelper.getContext().getString(android.R.string.ok); + } + if (mNegativeButtonText == null) { + mNegativeButtonText = mHelper.getContext().getString(android.R.string.cancel); + } + + return new PermissionRequest( + mHelper, + mPerms, + mRequestCode, + mRationale, + mPositiveButtonText, + mNegativeButtonText, + mTheme); + } + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogClickListener.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogClickListener.java new file mode 100644 index 0000000..4543bd3 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogClickListener.java @@ -0,0 +1,77 @@ +package com.chwl.library.easypermisssion; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import androidx.fragment.app.Fragment; + +import java.util.Arrays; + +import com.chwl.library.easypermisssion.helper.PermissionHelper; + +/** + * Click listener for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}. + */ +class RationaleDialogClickListener implements Dialog.OnClickListener { + + private Object mHost; + private RationaleDialogConfig mConfig; + private EasyPermissions.PermissionCallbacks mCallbacks; + private EasyPermissions.RationaleCallbacks mRationaleCallbacks; + + RationaleDialogClickListener(RationaleDialogFragmentCompat compatDialogFragment, + RationaleDialogConfig config, + EasyPermissions.PermissionCallbacks callbacks, + EasyPermissions.RationaleCallbacks rationaleCallbacks) { + + mHost = compatDialogFragment.getParentFragment() != null + ? compatDialogFragment.getParentFragment() + : compatDialogFragment.getActivity(); + + mConfig = config; + mCallbacks = callbacks; + mRationaleCallbacks = rationaleCallbacks; + + } + + RationaleDialogClickListener(RationaleDialogFragment dialogFragment, + RationaleDialogConfig config, + EasyPermissions.PermissionCallbacks callbacks, + EasyPermissions.RationaleCallbacks dialogCallback) { + + mHost = dialogFragment.getActivity(); + + mConfig = config; + mCallbacks = callbacks; + mRationaleCallbacks = dialogCallback; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + int requestCode = mConfig.requestCode; + if (which == Dialog.BUTTON_POSITIVE) { + String[] permissions = mConfig.permissions; + if (mRationaleCallbacks != null) { + mRationaleCallbacks.onRationaleAccepted(requestCode); + } + if (mHost instanceof Fragment) { + PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions); + } else if (mHost instanceof Activity) { + PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions); + } else { + throw new RuntimeException("Host must be an Activity or Fragment!"); + } + } else { + if (mRationaleCallbacks != null) { + mRationaleCallbacks.onRationaleDenied(requestCode); + } + notifyPermissionDenied(); + } + } + + private void notifyPermissionDenied() { + if (mCallbacks != null) { + mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions)); + } + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogConfig.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogConfig.java new file mode 100644 index 0000000..bf1f06c --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogConfig.java @@ -0,0 +1,143 @@ +package com.chwl.library.easypermisssion; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AlertDialog; + +import com.chwl.library.R; + +/** + * Configuration for either {@link RationaleDialogFragment} or {@link RationaleDialogFragmentCompat}. + */ +class RationaleDialogConfig { + + private static final String KEY_POSITIVE_BUTTON = "positiveButton"; + private static final String KEY_NEGATIVE_BUTTON = "negativeButton"; + private static final String KEY_RATIONALE_MESSAGE = "rationaleMsg"; + private static final String KEY_THEME = "theme"; + private static final String KEY_REQUEST_CODE = "requestCode"; + private static final String KEY_PERMISSIONS = "permissions"; + + String positiveButton; + String negativeButton; + int theme; + int requestCode; + String rationaleMsg; + String[] permissions; + + RationaleDialogConfig(@NonNull String positiveButton, + @NonNull String negativeButton, + @NonNull String rationaleMsg, + @StyleRes int theme, + int requestCode, + @NonNull String[] permissions) { + + this.positiveButton = positiveButton; + this.negativeButton = negativeButton; + this.rationaleMsg = rationaleMsg; + this.theme = theme; + this.requestCode = requestCode; + this.permissions = permissions; + } + + RationaleDialogConfig(Bundle bundle) { + positiveButton = bundle.getString(KEY_POSITIVE_BUTTON); + negativeButton = bundle.getString(KEY_NEGATIVE_BUTTON); + rationaleMsg = bundle.getString(KEY_RATIONALE_MESSAGE); + theme = bundle.getInt(KEY_THEME); + requestCode = bundle.getInt(KEY_REQUEST_CODE); + permissions = bundle.getStringArray(KEY_PERMISSIONS); + } + + Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putString(KEY_POSITIVE_BUTTON, positiveButton); + bundle.putString(KEY_NEGATIVE_BUTTON, negativeButton); + bundle.putString(KEY_RATIONALE_MESSAGE, rationaleMsg); + bundle.putInt(KEY_THEME, theme); + bundle.putInt(KEY_REQUEST_CODE, requestCode); + bundle.putStringArray(KEY_PERMISSIONS, permissions); + + return bundle; + } + + Dialog createDialog(Context context, Dialog.OnClickListener listener) { + Dialog dialog = new Dialog(context, R.style.Dialog_Transparent); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + View view = LayoutInflater.from(context).inflate(R.layout.layout_permission_rationale_dialog, null); + View tv_rationale = view.findViewById(R.id.tv_rationale); + if (tv_rationale instanceof TextView) { + ((TextView) tv_rationale).setText(rationaleMsg); + } + View btn_cancel = view.findViewById(R.id.btn_cancel); + if (btn_cancel instanceof Button) { + ((Button) btn_cancel).setText(negativeButton); + btn_cancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + if (listener != null) { + listener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE); + } + } + }); + } + View btn_ok = view.findViewById(R.id.btn_ok); + if (btn_ok instanceof Button) { + ((Button) btn_ok).setText(positiveButton); + btn_ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + if (listener != null) { + listener.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + } + } + }); + } + dialog.setContentView(view); + dialog.create(); + return dialog; + } + + AlertDialog createSupportDialog(Context context, Dialog.OnClickListener listener) { + AlertDialog.Builder builder; + if (theme > 0) { + builder = new AlertDialog.Builder(context, theme); + } else { + builder = new AlertDialog.Builder(context); + } + return builder + .setCancelable(false) + .setPositiveButton(positiveButton, listener) + .setNegativeButton(negativeButton, listener) + .setMessage(rationaleMsg) + .create(); + } + + android.app.AlertDialog createFrameworkDialog(Context context, Dialog.OnClickListener listener) { + android.app.AlertDialog.Builder builder; + if (theme > 0) { + builder = new android.app.AlertDialog.Builder(context, theme); + } else { + builder = new android.app.AlertDialog.Builder(context); + } + return builder + .setCancelable(false) + .setPositiveButton(positiveButton, listener) + .setNegativeButton(negativeButton, listener) + .setMessage(rationaleMsg) + .create(); + } + +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragment.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragment.java new file mode 100644 index 0000000..dcfb5c7 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragment.java @@ -0,0 +1,113 @@ +package com.chwl.library.easypermisssion; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.StyleRes; + +/** + * {@link DialogFragment} to display rationale for permission requests when the request comes from + * a Fragment or Activity that can host a Fragment. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class RationaleDialogFragment extends DialogFragment { + + public static final String TAG = "RationaleDialogFragment"; + + private EasyPermissions.PermissionCallbacks mPermissionCallbacks; + private EasyPermissions.RationaleCallbacks mRationaleCallbacks; + private boolean mStateSaved = false; + + public static RationaleDialogFragment newInstance( + @NonNull String positiveButton, + @NonNull String negativeButton, + @NonNull String rationaleMsg, + @StyleRes int theme, + int requestCode, + @NonNull String[] permissions) { + + // Create new Fragment + RationaleDialogFragment dialogFragment = new RationaleDialogFragment(); + + // Initialize configuration as arguments + RationaleDialogConfig config = new RationaleDialogConfig( + positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions); + dialogFragment.setArguments(config.toBundle()); + + return dialogFragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && getParentFragment() != null) { + if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) { + mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment(); + } + if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){ + mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment(); + } + + } + + if (context instanceof EasyPermissions.PermissionCallbacks) { + mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context; + } + + if (context instanceof EasyPermissions.RationaleCallbacks) { + mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + mStateSaved = true; + super.onSaveInstanceState(outState); + } + + /** + * Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException + * would otherwise occur. + */ + public void showAllowingStateLoss(FragmentManager manager, String tag) { + // API 26 added this convenient method + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (manager.isStateSaved()) { + return; + } + } + + if (mStateSaved) { + return; + } + + show(manager, tag); + } + + @Override + public void onDetach() { + super.onDetach(); + mPermissionCallbacks = null; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Rationale dialog should not be cancelable + setCancelable(false); + + // Get config from arguments, create click listener + RationaleDialogConfig config = new RationaleDialogConfig(getArguments()); + RationaleDialogClickListener clickListener = + new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks); + + // Create an AlertDialog + return config.createDialog(getActivity(), clickListener); + } + +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragmentCompat.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragmentCompat.java new file mode 100644 index 0000000..20077c1 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/RationaleDialogFragmentCompat.java @@ -0,0 +1,97 @@ +package com.chwl.library.easypermisssion; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.StyleRes; +import androidx.fragment.app.FragmentManager; +import androidx.appcompat.app.AppCompatDialogFragment; + +/** + * {@link AppCompatDialogFragment} to display rationale for permission requests when the request + * comes from a Fragment or Activity that can host a Fragment. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class RationaleDialogFragmentCompat extends AppCompatDialogFragment { + + public static final String TAG = "RationaleDialogFragmentCompat"; + + private EasyPermissions.PermissionCallbacks mPermissionCallbacks; + private EasyPermissions.RationaleCallbacks mRationaleCallbacks; + + public static RationaleDialogFragmentCompat newInstance( + @NonNull String rationaleMsg, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String[] permissions) { + + // Create new Fragment + RationaleDialogFragmentCompat dialogFragment = new RationaleDialogFragmentCompat(); + + // Initialize configuration as arguments + RationaleDialogConfig config = new RationaleDialogConfig( + positiveButton, negativeButton, rationaleMsg, theme, requestCode, permissions); + dialogFragment.setArguments(config.toBundle()); + + return dialogFragment; + } + + /** + * Version of {@link #show(FragmentManager, String)} that no-ops when an IllegalStateException + * would otherwise occur. + */ + public void showAllowingStateLoss(FragmentManager manager, String tag) { + if (manager.isStateSaved()) { + return; + } + + show(manager, tag); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getParentFragment() != null) { + if (getParentFragment() instanceof EasyPermissions.PermissionCallbacks) { + mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) getParentFragment(); + } + if (getParentFragment() instanceof EasyPermissions.RationaleCallbacks){ + mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) getParentFragment(); + } + } + + if (context instanceof EasyPermissions.PermissionCallbacks) { + mPermissionCallbacks = (EasyPermissions.PermissionCallbacks) context; + } + + if (context instanceof EasyPermissions.RationaleCallbacks) { + mRationaleCallbacks = (EasyPermissions.RationaleCallbacks) context; + } + } + + @Override + public void onDetach() { + super.onDetach(); + mPermissionCallbacks = null; + mRationaleCallbacks = null; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Rationale dialog should not be cancelable + setCancelable(false); + + // Get config from arguments, create click listener + RationaleDialogConfig config = new RationaleDialogConfig(getArguments()); + RationaleDialogClickListener clickListener = + new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks); + + // Create an AlertDialog + return config.createDialog(getContext(), clickListener); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/ActivityPermissionHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/ActivityPermissionHelper.java new file mode 100644 index 0000000..25d42a7 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/ActivityPermissionHelper.java @@ -0,0 +1,59 @@ +package com.chwl.library.easypermisssion.helper; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.core.app.ActivityCompat; +import android.util.Log; + +import com.chwl.library.easypermisssion.RationaleDialogFragment; + +/** + * Permissions helper for {@link Activity}. + */ +class ActivityPermissionHelper extends PermissionHelper { + private static final String TAG = "ActPermissionHelper"; + + public ActivityPermissionHelper(Activity host) { + super(host); + } + + @Override + public void directRequestPermissions(int requestCode, @NonNull String... perms) { + ActivityCompat.requestPermissions(getHost(), perms, requestCode); + } + + @Override + public boolean shouldShowRequestPermissionRationale(@NonNull String perm) { + return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm); + } + + @Override + public Context getContext() { + return getHost(); + } + + @Override + public void showRequestPermissionRationale(@NonNull String rationale, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String... perms) { + FragmentManager fm = getHost().getFragmentManager(); + + // Check if fragment is already showing + Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG); + if (fragment instanceof RationaleDialogFragment) { + Log.d(TAG, "Found existing fragment, not showing rationale."); + return; + } + + RationaleDialogFragment + .newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms) + .showAllowingStateLoss(fm, RationaleDialogFragment.TAG); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/AppCompatActivityPermissionsHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/AppCompatActivityPermissionsHelper.java new file mode 100644 index 0000000..e5fbdd8 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/AppCompatActivityPermissionsHelper.java @@ -0,0 +1,37 @@ +package com.chwl.library.easypermisssion.helper; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.FragmentManager; +import androidx.appcompat.app.AppCompatActivity; + +/** + * Permissions helper for {@link AppCompatActivity}. + */ +class AppCompatActivityPermissionsHelper extends BaseSupportPermissionsHelper { + + public AppCompatActivityPermissionsHelper(AppCompatActivity host) { + super(host); + } + + @Override + public FragmentManager getSupportFragmentManager() { + return getHost().getSupportFragmentManager(); + } + + @Override + public void directRequestPermissions(int requestCode, @NonNull String... perms) { + ActivityCompat.requestPermissions(getHost(), perms, requestCode); + } + + @Override + public boolean shouldShowRequestPermissionRationale(@NonNull String perm) { + return ActivityCompat.shouldShowRequestPermissionRationale(getHost(), perm); + } + + @Override + public Context getContext() { + return getHost(); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/BaseSupportPermissionsHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/BaseSupportPermissionsHelper.java new file mode 100644 index 0000000..b864d1f --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/BaseSupportPermissionsHelper.java @@ -0,0 +1,45 @@ +package com.chwl.library.easypermisssion.helper; + +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import android.util.Log; + +import com.chwl.library.easypermisssion.RationaleDialogFragmentCompat; + +/** + * Implementation of {@link PermissionHelper} for Support Library host classes. + */ +public abstract class BaseSupportPermissionsHelper extends PermissionHelper { + + private static final String TAG = "BSPermissionsHelper"; + + public BaseSupportPermissionsHelper(@NonNull T host) { + super(host); + } + + public abstract FragmentManager getSupportFragmentManager(); + + @Override + public void showRequestPermissionRationale(@NonNull String rationale, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String... perms) { + + FragmentManager fm = getSupportFragmentManager(); + + // Check if fragment is already showing + Fragment fragment = fm.findFragmentByTag(RationaleDialogFragmentCompat.TAG); + if (fragment instanceof RationaleDialogFragmentCompat) { + Log.d(TAG, "Found existing fragment, not showing rationale."); + return; + } + + RationaleDialogFragmentCompat + .newInstance(rationale, positiveButton, negativeButton, theme, requestCode, perms) + .showAllowingStateLoss(fm, RationaleDialogFragmentCompat.TAG); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/LowApiPermissionsHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/LowApiPermissionsHelper.java new file mode 100644 index 0000000..1e41374 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/LowApiPermissionsHelper.java @@ -0,0 +1,47 @@ +package com.chwl.library.easypermisssion.helper; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; + +/** + * Permissions helper for apps built against API < 23, which do not need runtime permissions. + */ +class LowApiPermissionsHelper extends PermissionHelper { + public LowApiPermissionsHelper(@NonNull T host) { + super(host); + } + + @Override + public void directRequestPermissions(int requestCode, @NonNull String... perms) { + throw new IllegalStateException("Should never be requesting permissions on API < 23!"); + } + + @Override + public boolean shouldShowRequestPermissionRationale(@NonNull String perm) { + return false; + } + + @Override + public void showRequestPermissionRationale(@NonNull String rationale, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String... perms) { + throw new IllegalStateException("Should never be requesting permissions on API < 23!"); + } + + @Override + public Context getContext() { + if (getHost() instanceof Activity) { + return (Context) getHost(); + } else if (getHost() instanceof Fragment) { + return ((Fragment) getHost()).getContext(); + } else { + throw new IllegalStateException("Unknown host: " + getHost()); + } + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/PermissionHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/PermissionHelper.java new file mode 100644 index 0000000..677c474 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/PermissionHelper.java @@ -0,0 +1,113 @@ +package com.chwl.library.easypermisssion.helper; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.List; + +/** + * Delegate class to make permission calls based on the 'host' (Fragment, Activity, etc). + */ +public abstract class PermissionHelper { + + private T mHost; + + @NonNull + public static PermissionHelper newInstance(Activity host) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return new LowApiPermissionsHelper<>(host); + } + + if (host instanceof AppCompatActivity) + return new AppCompatActivityPermissionsHelper((AppCompatActivity) host); + else { + return new ActivityPermissionHelper(host); + } + } + + @NonNull + public static PermissionHelper newInstance(Fragment host) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return new LowApiPermissionsHelper<>(host); + } + + return new SupportFragmentPermissionHelper(host); + } + + // ============================================================================ + // Public concrete methods + // ============================================================================ + + public PermissionHelper(@NonNull T host) { + mHost = host; + } + + private boolean shouldShowRationale(@NonNull String... perms) { + for (String perm : perms) { + if (shouldShowRequestPermissionRationale(perm)) { + return true; + } + } + return false; + } + + public void requestPermissions(@NonNull String rationale, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String... perms) { + if (shouldShowRationale(perms)) { + showRequestPermissionRationale( + rationale, positiveButton, negativeButton, theme, requestCode, perms); + } else { + directRequestPermissions(requestCode, perms); + } + } + + public boolean somePermissionPermanentlyDenied(@NonNull List perms) { + for (String deniedPermission : perms) { + if (permissionPermanentlyDenied(deniedPermission)) { + return true; + } + } + + return false; + } + + public boolean permissionPermanentlyDenied(@NonNull String perms) { + return !shouldShowRequestPermissionRationale(perms); + } + + public boolean somePermissionDenied(@NonNull String... perms) { + return shouldShowRationale(perms); + } + + @NonNull + public T getHost() { + return mHost; + } + + // ============================================================================ + // Public abstract methods + // ============================================================================ + + public abstract void directRequestPermissions(int requestCode, @NonNull String... perms); + + public abstract boolean shouldShowRequestPermissionRationale(@NonNull String perm); + + public abstract void showRequestPermissionRationale(@NonNull String rationale, + @NonNull String positiveButton, + @NonNull String negativeButton, + @StyleRes int theme, + int requestCode, + @NonNull String... perms); + + public abstract Context getContext(); + +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/SupportFragmentPermissionHelper.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/SupportFragmentPermissionHelper.java new file mode 100644 index 0000000..21988a4 --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/SupportFragmentPermissionHelper.java @@ -0,0 +1,36 @@ +package com.chwl.library.easypermisssion.helper; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +/** + * Permissions helper for {@link Fragment} from the support library. + */ +class SupportFragmentPermissionHelper extends BaseSupportPermissionsHelper { + + public SupportFragmentPermissionHelper(@NonNull Fragment host) { + super(host); + } + + @Override + public FragmentManager getSupportFragmentManager() { + return getHost().getChildFragmentManager(); + } + + @Override + public void directRequestPermissions(int requestCode, @NonNull String... perms) { + getHost().requestPermissions(perms, requestCode); + } + + @Override + public boolean shouldShowRequestPermissionRationale(@NonNull String perm) { + return getHost().shouldShowRequestPermissionRationale(perm); + } + + @Override + public Context getContext() { + return getHost().getActivity(); + } +} diff --git a/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/package-info.java b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/package-info.java new file mode 100644 index 0000000..2cec35c --- /dev/null +++ b/library/src/module_easypermission/java/com/chwl/library/easypermisssion/helper/package-info.java @@ -0,0 +1,4 @@ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +package com.chwl.library.easypermisssion.helper; + +import androidx.annotation.RestrictTo; diff --git a/library/src/module_easypermission/res/drawable/bg_common_cancel.xml b/library/src/module_easypermission/res/drawable/bg_common_cancel.xml new file mode 100644 index 0000000..00e7c95 --- /dev/null +++ b/library/src/module_easypermission/res/drawable/bg_common_cancel.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/library/src/module_easypermission/res/drawable/bg_common_confirm_normal.xml b/library/src/module_easypermission/res/drawable/bg_common_confirm_normal.xml new file mode 100644 index 0000000..8796b33 --- /dev/null +++ b/library/src/module_easypermission/res/drawable/bg_common_confirm_normal.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/library/src/module_easypermission/res/drawable/shape_white_20dp_round.xml b/library/src/module_easypermission/res/drawable/shape_white_20dp_round.xml new file mode 100644 index 0000000..db705e3 --- /dev/null +++ b/library/src/module_easypermission/res/drawable/shape_white_20dp_round.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/library/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml b/library/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml new file mode 100644 index 0000000..eef031e --- /dev/null +++ b/library/src/module_easypermission/res/layout/layout_permission_rationale_dialog.xml @@ -0,0 +1,53 @@ + + + + + + + +