Repository: Tencent/QMUI_Android
Branch: master
Commit: 026e7d486677
Files: 758
Total size: 4.0 MB
Directory structure:
gitextract_k1p2gcqq/
├── .github/
│ └── ISSUE_TEMPLATE.md
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.TXT
├── README.md
├── arch/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── qmui/
│ │ └── arch/
│ │ └── ExampleInstrumentedTest.java
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── qmuiteam/
│ │ │ └── qmui/
│ │ │ └── arch/
│ │ │ ├── InnerBaseActivity.java
│ │ │ ├── QMUIActivity.java
│ │ │ ├── QMUIFragment.java
│ │ │ ├── QMUIFragmentActivity.java
│ │ │ ├── QMUIFragmentContainerProvider.java
│ │ │ ├── QMUIFragmentPagerAdapter.java
│ │ │ ├── QMUILatestVisit.java
│ │ │ ├── QMUINavFragment.java
│ │ │ ├── QMUISwipeBackActivityManager.java
│ │ │ ├── SwipeBackLayout.java
│ │ │ ├── SwipeBackgroundView.java
│ │ │ ├── Utils.java
│ │ │ ├── annotation/
│ │ │ │ └── DefaultFirstFragment.java
│ │ │ ├── effect/
│ │ │ │ ├── Effect.java
│ │ │ │ ├── FragmentResultEffect.java
│ │ │ │ ├── MapEffect.java
│ │ │ │ ├── QMUIFragmentEffectHandler.java
│ │ │ │ ├── QMUIFragmentEffectRegistration.java
│ │ │ │ ├── QMUIFragmentEffectRegistry.java
│ │ │ │ ├── QMUIFragmentMapEffectHandler.java
│ │ │ │ └── QMUIFragmentResultEffectHandler.java
│ │ │ ├── record/
│ │ │ │ ├── DefaultLatestVisitStorage.java
│ │ │ │ ├── LatestVisitArgumentCollector.java
│ │ │ │ ├── QMUILatestVisitStorage.java
│ │ │ │ ├── RecordArgumentEditor.java
│ │ │ │ ├── RecordArgumentEditorImpl.java
│ │ │ │ └── RecordIdClassMap.java
│ │ │ └── scheme/
│ │ │ ├── ActivitySchemeItem.kt
│ │ │ ├── FragmentSchemeItem.kt
│ │ │ ├── QMUISchemeBuilder.kt
│ │ │ ├── QMUISchemeFragmentFactory.kt
│ │ │ ├── QMUISchemeHandler.kt
│ │ │ ├── QMUISchemeHandlerInterceptor.kt
│ │ │ ├── QMUISchemeIntentFactory.kt
│ │ │ ├── QMUISchemeMatcher.kt
│ │ │ ├── QMUIUnknownSchemeHandler.kt
│ │ │ ├── SchemeHandleContext.kt
│ │ │ ├── SchemeInfo.kt
│ │ │ ├── SchemeItem.kt
│ │ │ ├── SchemeMap.kt
│ │ │ ├── SchemeRefreshable.kt
│ │ │ └── SchemeValue.kt
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── decelerate_factor_interpolator.xml
│ │ │ ├── decelerate_low_factor_interpolator.xml
│ │ │ ├── scale_enter.xml
│ │ │ ├── scale_exit.xml
│ │ │ ├── slide_in_left.xml
│ │ │ ├── slide_in_right.xml
│ │ │ ├── slide_out_left.xml
│ │ │ ├── slide_out_right.xml
│ │ │ ├── slide_still.xml
│ │ │ ├── swipe_back_enter.xml
│ │ │ ├── swipe_back_exit.xml
│ │ │ └── swipe_back_exit_still.xml
│ │ ├── animator/
│ │ │ ├── scale_enter.xml
│ │ │ ├── scale_exit.xml
│ │ │ ├── slide_in_left.xml
│ │ │ ├── slide_in_right.xml
│ │ │ ├── slide_out_left.xml
│ │ │ ├── slide_out_right.xml
│ │ │ └── slide_still.xml
│ │ └── values/
│ │ ├── attrs.xml
│ │ ├── ids.xml
│ │ ├── qmui_integers.xml
│ │ ├── strings.xml
│ │ └── style.xml
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── qmui/
│ └── arch/
│ └── ExampleUnitTest.java
├── arch-annotation/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── qmui/
│ └── arch/
│ └── annotation/
│ ├── ActivityScheme.java
│ ├── FragmentContainerParam.java
│ ├── FragmentScheme.java
│ └── LatestVisitRecord.java
├── arch-compiler/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── qmui/
│ └── arch/
│ ├── BaseProcessor.java
│ ├── LatestVisitProcessor.java
│ └── SchemeProcessor.java
├── build.gradle.kts
├── compiler/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── qmuidemo/
│ └── compiler/
│ └── WidgetProcessor.java
├── compose/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── compose/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── qmuiteam/
│ │ │ └── compose/
│ │ │ └── modal/
│ │ │ ├── ModalImpl.kt
│ │ │ ├── QMUIBottomSheet.kt
│ │ │ ├── QMUIDialog.kt
│ │ │ ├── QMUIModal.kt
│ │ │ └── QMUIToast.kt
│ │ └── res/
│ │ └── values/
│ │ └── ids.xml
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── compose/
│ └── ExampleUnitTest.kt
├── compose-core/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── compose/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── qmuiteam/
│ │ │ └── compose/
│ │ │ └── core/
│ │ │ ├── ex/
│ │ │ │ └── DrawScopeEx.kt
│ │ │ ├── helper/
│ │ │ │ ├── Dimen.kt
│ │ │ │ ├── Global.kt
│ │ │ │ ├── Log.kt
│ │ │ │ └── LogTag.kt
│ │ │ ├── provider/
│ │ │ │ └── WindowInsets.kt
│ │ │ └── ui/
│ │ │ ├── DefaultConfig.kt
│ │ │ ├── PressWithAlphaBox.kt
│ │ │ ├── QMUIIcon.kt
│ │ │ ├── QMUIItem.kt
│ │ │ └── QMUITopBar.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_qmui_checkbox_checked.xml
│ │ │ ├── ic_qmui_checkbox_normal.xml
│ │ │ ├── ic_qmui_checkbox_partial.xml
│ │ │ ├── ic_qmui_chevron.xml
│ │ │ ├── ic_qmui_mark.xml
│ │ │ └── ic_qmui_topbar_back.xml
│ │ └── values/
│ │ └── qmui_ids.xml
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── compose/
│ └── ExampleUnitTest.kt
├── deploy.sh
├── editor/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── editor/
│ ├── EditorBehavior.kt
│ ├── QMUIEditor.kt
│ ├── Range.kt
│ ├── TextFieldValueEx.kt
│ └── WordEdit.kt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── lib/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── qmuidemo/
│ └── lib/
│ ├── Group.java
│ └── annotation/
│ └── Widget.java
├── photo/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── qmuiteam/
│ │ │ └── photo/
│ │ │ ├── activity/
│ │ │ │ ├── QMUIPhotoClipActivity.kt
│ │ │ │ ├── QMUIPhotoPickerActivity.kt
│ │ │ │ └── QMUIPhotoViewerActivity.kt
│ │ │ ├── compose/
│ │ │ │ ├── BitmapRegion.kt
│ │ │ │ ├── GesturePhoto.kt
│ │ │ │ ├── Loading.kt
│ │ │ │ ├── PhotoClipper.kt
│ │ │ │ ├── PhotoConfig.kt
│ │ │ │ ├── Thumbnail.kt
│ │ │ │ └── picker/
│ │ │ │ ├── Buckets.kt
│ │ │ │ ├── Common.kt
│ │ │ │ ├── Config.kt
│ │ │ │ ├── Edit.kt
│ │ │ │ ├── Grid.kt
│ │ │ │ ├── PaintEdit.kt
│ │ │ │ ├── Preview.kt
│ │ │ │ ├── TextEdit.kt
│ │ │ │ └── TopBarItem.kt
│ │ │ ├── data/
│ │ │ │ ├── QMUIBitmapRegion.kt
│ │ │ │ ├── QMUIMediaDataProvider.kt
│ │ │ │ ├── QMUIPhotoTransitionDelivery.kt
│ │ │ │ └── QMUIPhotoTransitionInfo.kt
│ │ │ ├── util/
│ │ │ │ ├── BitmapEx.kt
│ │ │ │ ├── QMUIPhotoHelper.kt
│ │ │ │ └── ViewEx.kt
│ │ │ └── vm/
│ │ │ └── QMUIPhotoPickerViewModel.kt
│ │ └── res/
│ │ └── anim/
│ │ ├── scale_enter.xml
│ │ └── scale_exit.xml
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── ExampleUnitTest.kt
├── photo-coil/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── photo/
│ │ └── coil/
│ │ ├── QMUICoilImageDecoderFactory.kt
│ │ ├── QMUICoilPhoto.kt
│ │ └── QMUIMediaCoilPhotoProviderFactory.kt
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── ExampleUnitTest.kt
├── photo-glide/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── photo/
│ │ └── glide/
│ │ ├── QMUIGlideModule.kt
│ │ ├── QMUIGlidePhoto.kt
│ │ └── QMUIMediaGlidePhotoProviderFactory.kt
│ └── test/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── ExampleUnitTest.kt
├── plugin/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── qmuiteam/
│ └── plugin/
│ ├── Dep.kt
│ ├── QMUIDepPlugin.kt
│ └── QMUIPublish.kt
├── qmui/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── assets/
│ │ └── QMUIWebviewBridge.js
│ ├── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── qmui/
│ │ ├── Beta.java
│ │ ├── QMUIConfig.java
│ │ ├── QMUIInterpolatorStaticHolder.java
│ │ ├── QMUILog.java
│ │ ├── alpha/
│ │ │ ├── QMUIAlphaButton.java
│ │ │ ├── QMUIAlphaConstraintLayout.java
│ │ │ ├── QMUIAlphaFrameLayout.java
│ │ │ ├── QMUIAlphaImageButton.java
│ │ │ ├── QMUIAlphaLinearLayout.java
│ │ │ ├── QMUIAlphaRelativeLayout.java
│ │ │ ├── QMUIAlphaTextView.java
│ │ │ ├── QMUIAlphaViewHelper.java
│ │ │ └── QMUIAlphaViewInf.java
│ │ ├── exposure/
│ │ │ ├── Exposure.kt
│ │ │ ├── ExposureChecker.kt
│ │ │ ├── ExposureContainer.kt
│ │ │ ├── ExposureEffect.kt
│ │ │ └── ExposureEx.kt
│ │ ├── kotlin/
│ │ │ ├── DimenKt.kt
│ │ │ ├── LayoutParamKt.kt
│ │ │ └── ViewKt.kt
│ │ ├── layout/
│ │ │ ├── IQMUILayout.java
│ │ │ ├── QMUIButton.java
│ │ │ ├── QMUIConstraintLayout.java
│ │ │ ├── QMUIFrameLayout.java
│ │ │ ├── QMUILayoutHelper.java
│ │ │ ├── QMUILinearLayout.java
│ │ │ ├── QMUIPriorityLinearLayout.java
│ │ │ └── QMUIRelativeLayout.java
│ │ ├── link/
│ │ │ ├── ITouchableSpan.java
│ │ │ ├── QMUILinkTouchDecorHelper.java
│ │ │ ├── QMUILinkTouchMovementMethod.java
│ │ │ ├── QMUILinkify.java
│ │ │ └── QMUIScrollingMovementMethod.java
│ │ ├── nestedScroll/
│ │ │ ├── IQMUIContinuousNestedBottomView.java
│ │ │ ├── IQMUIContinuousNestedScrollCommon.java
│ │ │ ├── IQMUIContinuousNestedTopView.java
│ │ │ ├── QMUIContinuousNestedBottomAreaBehavior.java
│ │ │ ├── QMUIContinuousNestedBottomDelegateLayout.java
│ │ │ ├── QMUIContinuousNestedBottomRecyclerView.java
│ │ │ ├── QMUIContinuousNestedScrollLayout.java
│ │ │ ├── QMUIContinuousNestedTopAreaBehavior.java
│ │ │ ├── QMUIContinuousNestedTopDelegateLayout.java
│ │ │ ├── QMUIContinuousNestedTopLinearLayout.java
│ │ │ ├── QMUIContinuousNestedTopRecyclerView.java
│ │ │ ├── QMUIContinuousNestedTopWebView.java
│ │ │ ├── QMUIDraggableScrollBar.java
│ │ │ └── QMUIViewOffsetBehavior.java
│ │ ├── qqface/
│ │ │ ├── IQMUIQQFaceManager.java
│ │ │ ├── QMUINoQQFaceManager.java
│ │ │ ├── QMUIQQFaceCompiler.java
│ │ │ ├── QMUIQQFaceView.java
│ │ │ └── QQFace.java
│ │ ├── recyclerView/
│ │ │ ├── QMUIRVDraggableScrollBar.java
│ │ │ ├── QMUIRVItemSwipeAction.java
│ │ │ ├── QMUISwipeAction.java
│ │ │ └── QMUISwipeViewHolder.java
│ │ ├── skin/
│ │ │ ├── IQMUISkinApplyListener.java
│ │ │ ├── IQMUISkinDispatchInterceptor.java
│ │ │ ├── IQMUISkinHandlerDecoration.java
│ │ │ ├── IQMUISkinHandlerSpan.java
│ │ │ ├── IQMUISkinHandlerView.java
│ │ │ ├── QMUISkinHelper.java
│ │ │ ├── QMUISkinLayoutInflaterFactory.java
│ │ │ ├── QMUISkinManager.java
│ │ │ ├── QMUISkinValueBuilder.java
│ │ │ ├── SkinWriter.java
│ │ │ ├── annotation/
│ │ │ │ ├── QMUISkinChangeNotAdapted.java
│ │ │ │ └── QMUISkinListenWithHierarchyChange.java
│ │ │ ├── defaultAttr/
│ │ │ │ ├── IQMUISkinDefaultAttrProvider.java
│ │ │ │ └── QMUISkinSimpleDefaultAttrProvider.java
│ │ │ └── handler/
│ │ │ ├── IQMUISkinRuleHandler.java
│ │ │ ├── QMUISkinRuleAlphaHandler.java
│ │ │ ├── QMUISkinRuleBackgroundHandler.java
│ │ │ ├── QMUISkinRuleBgTintColorHandler.java
│ │ │ ├── QMUISkinRuleBorderHandler.java
│ │ │ ├── QMUISkinRuleColorHandler.java
│ │ │ ├── QMUISkinRuleColorStateListHandler.java
│ │ │ ├── QMUISkinRuleDrawableHandler.java
│ │ │ ├── QMUISkinRuleFloatHandler.java
│ │ │ ├── QMUISkinRuleHintColorHandler.java
│ │ │ ├── QMUISkinRuleMoreBgColorHandler.java
│ │ │ ├── QMUISkinRuleMoreTextColorHandler.java
│ │ │ ├── QMUISkinRuleProgressColorHandler.java
│ │ │ ├── QMUISkinRuleSeparatorHandler.java
│ │ │ ├── QMUISkinRuleSrcHandler.java
│ │ │ ├── QMUISkinRuleTextColorHandler.java
│ │ │ ├── QMUISkinRuleTextCompoundSrcHandler.java
│ │ │ ├── QMUISkinRuleTextCompoundTintColorHandler.java
│ │ │ ├── QMUISkinRuleTintColorHandler.java
│ │ │ └── QMUISkinRuleUnderlineHandler.java
│ │ ├── span/
│ │ │ ├── QMUIAlignMiddleImageSpan.java
│ │ │ ├── QMUIBlockSpaceSpan.java
│ │ │ ├── QMUICustomTypefaceSpan.java
│ │ │ ├── QMUIMarginImageSpan.java
│ │ │ ├── QMUIOnSpanClickListener.java
│ │ │ ├── QMUITextSizeSpan.java
│ │ │ └── QMUITouchableSpan.java
│ │ ├── util/
│ │ │ ├── OnceReadValue.java
│ │ │ ├── QMUIActivityLifecycleCallbacks.java
│ │ │ ├── QMUICollapsingTextHelper.java
│ │ │ ├── QMUIColorHelper.java
│ │ │ ├── QMUIDeviceHelper.java
│ │ │ ├── QMUIDirection.java
│ │ │ ├── QMUIDisplayHelper.java
│ │ │ ├── QMUIDrawableHelper.java
│ │ │ ├── QMUIKeyboardHelper.java
│ │ │ ├── QMUILangHelper.java
│ │ │ ├── QMUINotchHelper.java
│ │ │ ├── QMUIPackageHelper.java
│ │ │ ├── QMUIReflectHelper.java
│ │ │ ├── QMUIResHelper.java
│ │ │ ├── QMUISpanHelper.java
│ │ │ ├── QMUIStatusBarHelper.java
│ │ │ ├── QMUIToastHelper.java
│ │ │ ├── QMUIViewHelper.java
│ │ │ ├── QMUIViewOffsetHelper.java
│ │ │ ├── QMUIWindowHelper.java
│ │ │ └── QMUIWindowInsetHelper.java
│ │ └── widget/
│ │ ├── IBlankTouchDetector.java
│ │ ├── IWindowInsetKeyboardConsumer.java
│ │ ├── QMUIAnimationListView.java
│ │ ├── QMUIAppBarLayout.java
│ │ ├── QMUICollapsingTopBarLayout.java
│ │ ├── QMUIEmptyView.java
│ │ ├── QMUIFloatLayout.java
│ │ ├── QMUIFontFitTextView.java
│ │ ├── QMUIItemViewsAdapter.java
│ │ ├── QMUILoadingView.java
│ │ ├── QMUINotchConsumeLayout.java
│ │ ├── QMUIObservableScrollView.java
│ │ ├── QMUIPagerAdapter.java
│ │ ├── QMUIProgressBar.java
│ │ ├── QMUIRadiusImageView.java
│ │ ├── QMUIRadiusImageView2.java
│ │ ├── QMUISeekBar.java
│ │ ├── QMUISlider.java
│ │ ├── QMUITopBar.java
│ │ ├── QMUITopBarLayout.java
│ │ ├── QMUIVerticalTextView.java
│ │ ├── QMUIViewPager.java
│ │ ├── QMUIWindowInsetLayout.java
│ │ ├── QMUIWindowInsetLayout2.java
│ │ ├── QMUIWrapContentListView.java
│ │ ├── QMUIWrapContentScrollView.java
│ │ ├── dialog/
│ │ │ ├── QMUIBaseDialog.java
│ │ │ ├── QMUIBottomSheet.java
│ │ │ ├── QMUIBottomSheetBaseBuilder.java
│ │ │ ├── QMUIBottomSheetBehavior.java
│ │ │ ├── QMUIBottomSheetGridItemModel.java
│ │ │ ├── QMUIBottomSheetGridItemView.java
│ │ │ ├── QMUIBottomSheetGridLineLayout.java
│ │ │ ├── QMUIBottomSheetListAdapter.java
│ │ │ ├── QMUIBottomSheetListItemDecoration.java
│ │ │ ├── QMUIBottomSheetListItemModel.java
│ │ │ ├── QMUIBottomSheetListItemView.java
│ │ │ ├── QMUIBottomSheetRootLayout.java
│ │ │ ├── QMUIDialog.java
│ │ │ ├── QMUIDialogAction.java
│ │ │ ├── QMUIDialogBlockBuilder.java
│ │ │ ├── QMUIDialogBuilder.java
│ │ │ ├── QMUIDialogMenuItemView.java
│ │ │ ├── QMUIDialogRootLayout.java
│ │ │ ├── QMUIDialogView.java
│ │ │ ├── QMUITipDialog.java
│ │ │ └── QMUITipDialogView.java
│ │ ├── grouplist/
│ │ │ ├── QMUICommonListItemView.java
│ │ │ ├── QMUIGroupListSectionHeaderFooterView.java
│ │ │ └── QMUIGroupListView.java
│ │ ├── popup/
│ │ │ ├── QMUIBasePopup.java
│ │ │ ├── QMUIFullScreenPopup.java
│ │ │ ├── QMUINormalPopup.java
│ │ │ ├── QMUIPopup.java
│ │ │ ├── QMUIPopups.java
│ │ │ └── QMUIQuickAction.java
│ │ ├── pullLayout/
│ │ │ ├── QMUIAlwaysFollowOffsetCalculator.java
│ │ │ ├── QMUICenterOffsetCalculator.java
│ │ │ ├── QMUIFixToTargetOffsetCalculator.java
│ │ │ ├── QMUIPullLayout.java
│ │ │ ├── QMUIPullLoadMoreView.java
│ │ │ └── QMUIPullRefreshView.java
│ │ ├── pullRefreshLayout/
│ │ │ ├── QMUICenterGravityRefreshOffsetCalculator.java
│ │ │ ├── QMUIDefaultRefreshOffsetCalculator.java
│ │ │ ├── QMUIFollowRefreshOffsetCalculator.java
│ │ │ └── QMUIPullRefreshLayout.java
│ │ ├── roundwidget/
│ │ │ ├── QMUIRoundButton.java
│ │ │ ├── QMUIRoundButtonDrawable.java
│ │ │ ├── QMUIRoundFrameLayout.java
│ │ │ ├── QMUIRoundLinearLayout.java
│ │ │ └── QMUIRoundRelativeLayout.java
│ │ ├── section/
│ │ │ ├── QMUIDefaultStickySectionAdapter.java
│ │ │ ├── QMUISection.java
│ │ │ ├── QMUISectionDiffCallback.java
│ │ │ ├── QMUIStickySectionAdapter.java
│ │ │ ├── QMUIStickySectionItemDecoration.java
│ │ │ └── QMUIStickySectionLayout.java
│ │ ├── tab/
│ │ │ ├── QMUIBasicTabSegment.java
│ │ │ ├── QMUITab.java
│ │ │ ├── QMUITabAdapter.java
│ │ │ ├── QMUITabBuilder.java
│ │ │ ├── QMUITabIcon.java
│ │ │ ├── QMUITabIndicator.java
│ │ │ ├── QMUITabSegment.java
│ │ │ ├── QMUITabSegment2.java
│ │ │ └── QMUITabView.java
│ │ ├── textview/
│ │ │ ├── ISpanTouchFix.java
│ │ │ ├── QMUILinkTextView.java
│ │ │ └── QMUISpanTouchFixTextView.java
│ │ └── webview/
│ │ ├── QMUIBridgeWebViewClient.java
│ │ ├── QMUIWebView.java
│ │ ├── QMUIWebViewBridgeHandler.java
│ │ ├── QMUIWebViewClient.java
│ │ └── QMUIWebViewContainer.java
│ └── res/
│ ├── anim/
│ │ ├── decelerate_factor_interpolator.xml
│ │ ├── decelerate_low_factor_interpolator.xml
│ │ ├── grow_from_bottom.xml
│ │ ├── grow_from_bottomleft_to_topright.xml
│ │ ├── grow_from_bottomright_to_topleft.xml
│ │ ├── grow_from_top.xml
│ │ ├── grow_from_topleft_to_bottomright.xml
│ │ ├── grow_from_topright_to_bottomleft.xml
│ │ ├── scale_in_center.xml
│ │ ├── scale_out_center.xml
│ │ ├── shrink_from_bottom.xml
│ │ ├── shrink_from_bottomleft_to_topright.xml
│ │ ├── shrink_from_bottomright_to_topleft.xml
│ │ ├── shrink_from_top.xml
│ │ ├── shrink_from_topleft_to_bottomright.xml
│ │ └── shrink_from_topright_to_bottomleft.xml
│ ├── color/
│ │ ├── qmui_btn_blue_bg.xml
│ │ ├── qmui_btn_blue_border.xml
│ │ ├── qmui_btn_blue_text.xml
│ │ ├── qmui_s_link_color.xml
│ │ ├── qmui_s_list_item_text_color.xml
│ │ ├── qmui_s_switch_text_color.xml
│ │ ├── qmui_s_transparent.xml
│ │ └── qmui_topbar_text_color.xml
│ ├── drawable/
│ │ ├── qmui_divider_bottom_bitmap.xml
│ │ ├── qmui_divider_top_bitmap.xml
│ │ ├── qmui_icon_popup_close.xml
│ │ ├── qmui_icon_popup_close_with_bg.xml
│ │ ├── qmui_icon_pull_down.xml
│ │ ├── qmui_icon_quick_action_more_arrow_left.xml
│ │ ├── qmui_icon_quick_action_more_arrow_right.xml
│ │ ├── qmui_icon_topbar_back.xml
│ │ ├── qmui_s_checkbox.xml
│ │ ├── qmui_s_icon_switch.xml
│ │ ├── qmui_s_list_item_bg_1.xml
│ │ ├── qmui_s_list_item_bg_2.xml
│ │ ├── qmui_s_switch_thumb.xml
│ │ ├── qmui_s_switch_track.xml
│ │ ├── qmui_switch_thumb.xml
│ │ ├── qmui_switch_thumb_checked.xml
│ │ ├── qmui_switch_track.xml
│ │ ├── qmui_switch_track_checked.xml
│ │ └── qmui_tips_point.xml
│ ├── drawable-v21/
│ │ └── qmui_s_list_item_bg_2.xml
│ ├── layout/
│ │ ├── qmui_bottom_sheet_dialog.xml
│ │ ├── qmui_common_list_item.xml
│ │ ├── qmui_empty_view.xml
│ │ └── qmui_group_list_section_layout.xml
│ ├── values/
│ │ ├── config_colors.xml
│ │ ├── qmui_attrs.xml
│ │ ├── qmui_attrs_alpha.xml
│ │ ├── qmui_attrs_base.xml
│ │ ├── qmui_attrs_custom.xml
│ │ ├── qmui_attrs_layout.xml
│ │ ├── qmui_attrs_round.xml
│ │ ├── qmui_colors.xml
│ │ ├── qmui_dimens.xml
│ │ ├── qmui_ids.xml
│ │ ├── qmui_strings.xml
│ │ ├── qmui_style_appearance.xml
│ │ ├── qmui_style_widget.xml
│ │ └── qmui_themes.xml
│ └── values-v21/
│ └── qmui_themes.xml
├── qmuidemo/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── lint.xml
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── assets/
│ │ └── demo.html
│ ├── java/
│ │ └── com/
│ │ └── qmuiteam/
│ │ └── qmuidemo/
│ │ ├── QDApplication.kt
│ │ ├── QDMainActivity.java
│ │ ├── QDQQFaceManager.java
│ │ ├── activity/
│ │ │ ├── ArchTestActivity.java
│ │ │ ├── LauncherActivity.kt
│ │ │ ├── QDPhotoPickerActivity.kt
│ │ │ ├── TestArchInViewPagerActivity.java
│ │ │ └── TranslucentActivity.java
│ │ ├── adaptor/
│ │ │ ├── QDRecyclerViewAdapter.java
│ │ │ └── QDSimpleAdapter.java
│ │ ├── base/
│ │ │ ├── BaseActivity.java
│ │ │ ├── BaseFragment.java
│ │ │ ├── BaseFragmentActivity.java
│ │ │ ├── BaseRecyclerAdapter.java
│ │ │ ├── ComposeBaseFragment.kt
│ │ │ └── RecyclerViewHolder.java
│ │ ├── decorator/
│ │ │ ├── DividerItemDecoration.java
│ │ │ └── GridDividerItemDecoration.java
│ │ ├── fragment/
│ │ │ ├── QDAboutFragment.kt
│ │ │ ├── QDDialogFragment.kt
│ │ │ ├── QDWebExplorerFragment.java
│ │ │ ├── components/
│ │ │ │ ├── QDBottomSheetFragment.java
│ │ │ │ ├── QDButtonFragment.kt
│ │ │ │ ├── QDCollapsingTopBarLayoutFragment.java
│ │ │ │ ├── QDEmptyViewFragment.java
│ │ │ │ ├── QDFloatLayoutFragment.java
│ │ │ │ ├── QDGroupListViewFragment.kt
│ │ │ │ ├── QDLayoutFragment.java
│ │ │ │ ├── QDLinkTextViewFragment.java
│ │ │ │ ├── QDPopupFragment.java
│ │ │ │ ├── QDPriorityLinearLayoutFragment.java
│ │ │ │ ├── QDProgressBarFragment.java
│ │ │ │ ├── QDPullRefreshFragment.kt
│ │ │ │ ├── QDRadiusImageView2ScaleTypeFragment.java
│ │ │ │ ├── QDRadiusImageView2UsageFragment.java
│ │ │ │ ├── QDRadiusImageViewFragment.java
│ │ │ │ ├── QDRadiusImageViewScaleTypeFragment.java
│ │ │ │ ├── QDRadiusImageViewUsageFragment.java
│ │ │ │ ├── QDRecyclerViewDraggableScrollBarFragment.java
│ │ │ │ ├── QDSliderFragment.java
│ │ │ │ ├── QDSpanTouchFixTextViewFragment.java
│ │ │ │ ├── QDTabSegment2FixModeFragment.java
│ │ │ │ ├── QDTabSegment2ScrollableModeFragment.java
│ │ │ │ ├── QDTabSegmentFixModeFragment.java
│ │ │ │ ├── QDTabSegmentFragment.java
│ │ │ │ ├── QDTabSegmentScrollableModeFragment.java
│ │ │ │ ├── QDTabSegmentSpaceWeightFragment.java
│ │ │ │ ├── QDTipDialogFragment.java
│ │ │ │ ├── QDVerticalTextViewFragment.java
│ │ │ │ ├── SliderSchemeMatcher.java
│ │ │ │ ├── pullLayout/
│ │ │ │ │ ├── QDPullFragment.java
│ │ │ │ │ ├── QDPullHorizontalTestFragment.java
│ │ │ │ │ ├── QDPullRefreshAndLoadMoreTestFragment.java
│ │ │ │ │ └── QDPullVerticalTestFragment.java
│ │ │ │ ├── qqface/
│ │ │ │ │ ├── QDQQFaceFragment.java
│ │ │ │ │ ├── QDQQFacePerformanceTestFragment.java
│ │ │ │ │ ├── QDQQFaceTestData.java
│ │ │ │ │ ├── QDQQFaceUsageFragment.kt
│ │ │ │ │ ├── emojicon/
│ │ │ │ │ │ ├── EmojiCache.java
│ │ │ │ │ │ ├── EmojiconHandler.java
│ │ │ │ │ │ ├── EmojiconSpan.java
│ │ │ │ │ │ ├── EmojiconTextView.java
│ │ │ │ │ │ └── emoji/
│ │ │ │ │ │ ├── Emojicon.java
│ │ │ │ │ │ ├── Nature.java
│ │ │ │ │ │ ├── Objects.java
│ │ │ │ │ │ ├── People.java
│ │ │ │ │ │ ├── Places.java
│ │ │ │ │ │ └── Symbols.java
│ │ │ │ │ └── pageView/
│ │ │ │ │ ├── QDEmojiconPagerView.java
│ │ │ │ │ ├── QDQQFaceBasePagerView.java
│ │ │ │ │ └── QDQQFacePagerView.java
│ │ │ │ ├── section/
│ │ │ │ │ ├── QDBaseSectionLayoutFragment.java
│ │ │ │ │ ├── QDGridSectionAdapter.java
│ │ │ │ │ ├── QDGridSectionLayoutFragment.java
│ │ │ │ │ ├── QDListSectionAdapter.java
│ │ │ │ │ ├── QDListSectionLayoutFragment.java
│ │ │ │ │ ├── QDListWithDecorationSectionAdapter.java
│ │ │ │ │ ├── QDListWithDecorationSectionLayoutFragment.java
│ │ │ │ │ └── QDSectionLayoutFragment.java
│ │ │ │ ├── swipeAction/
│ │ │ │ │ ├── QDRVSwipeActionFragment.java
│ │ │ │ │ ├── QDRVSwipeDeleteWithNoActionFragment.java
│ │ │ │ │ ├── QDRVSwipeMutiActionFragment.java
│ │ │ │ │ ├── QDRVSwipeMutiActionOnlyIconFragment.java
│ │ │ │ │ ├── QDRVSwipeMutiActionWithIconFragment.java
│ │ │ │ │ ├── QDRVSwipeSingleDeleteActionFragment.java
│ │ │ │ │ └── QDRVSwipeUpDeleteFragment.java
│ │ │ │ └── viewpager/
│ │ │ │ ├── CardTransformer.java
│ │ │ │ ├── QDFitSystemWindowViewPagerFragment.java
│ │ │ │ ├── QDLoopViewPagerFragment.java
│ │ │ │ └── QDViewPagerFragment.java
│ │ │ ├── home/
│ │ │ │ ├── HomeComponentsController.java
│ │ │ │ ├── HomeController.java
│ │ │ │ ├── HomeFragment.java
│ │ │ │ ├── HomeLabController.java
│ │ │ │ └── HomeUtilController.java
│ │ │ ├── lab/
│ │ │ │ ├── QDAnimationListViewFragment.java
│ │ │ │ ├── QDArchNavFragment.java
│ │ │ │ ├── QDArchSurfaceTestFragment.java
│ │ │ │ ├── QDArchTestFragment.java
│ │ │ │ ├── QDArchWebViewTestFragment.java
│ │ │ │ ├── QDComposeTipFragment.kt
│ │ │ │ ├── QDContinuousBottomView.java
│ │ │ │ ├── QDContinuousNestedScroll1Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll2Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll3Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll4Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll5Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll6Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll7Fragment.java
│ │ │ │ ├── QDContinuousNestedScroll8Fragment.java
│ │ │ │ ├── QDContinuousNestedScrollBaseFragment.java
│ │ │ │ ├── QDContinuousNestedScrollFragment.java
│ │ │ │ ├── QDEditorFragment.kt
│ │ │ │ ├── QDEmojiInputFragment.kt
│ │ │ │ ├── QDPhotoClipFragment.kt
│ │ │ │ ├── QDPhotoFragment.kt
│ │ │ │ ├── QDSchemeFragment.java
│ │ │ │ ├── QDSnapHelperFragment.java
│ │ │ │ ├── QDSwipeDeleteListViewFragment.java
│ │ │ │ ├── QDWebViewBridgeFragment.java
│ │ │ │ ├── QDWebViewFixFragment.java
│ │ │ │ └── QDWebViewFragment.java
│ │ │ └── util/
│ │ │ ├── QDColorHelperFragment.java
│ │ │ ├── QDDeviceHelperFragment.java
│ │ │ ├── QDDrawableHelperFragment.java
│ │ │ ├── QDNotchHelperFragment.java
│ │ │ ├── QDSpanFragment.java
│ │ │ ├── QDStatusBarHelperFragment.java
│ │ │ ├── QDViewHelperAnimationFadeFragment.java
│ │ │ ├── QDViewHelperAnimationSlideFragment.java
│ │ │ ├── QDViewHelperBackgroundAnimationBlinkFragment.java
│ │ │ ├── QDViewHelperBackgroundAnimationFullFragment.java
│ │ │ └── QDViewHelperFragment.java
│ │ ├── manager/
│ │ │ ├── QDAppGlideModule.kt
│ │ │ ├── QDDataManager.java
│ │ │ ├── QDPreferenceManager.java
│ │ │ ├── QDSchemeManager.kt
│ │ │ ├── QDSkinManager.java
│ │ │ ├── QDUpgradeManager.java
│ │ │ ├── UpgradeTask.java
│ │ │ └── UpgradeTipTask.java
│ │ ├── model/
│ │ │ ├── CustomEffect.java
│ │ │ ├── QDItemDescription.java
│ │ │ ├── SectionHeader.java
│ │ │ └── SectionItem.java
│ │ └── view/
│ │ ├── QDLoadingItemView.java
│ │ ├── QDSectionHeaderView.java
│ │ ├── QDShadowAdjustLayout.java
│ │ └── QDWebView.java
│ └── res/
│ ├── color/
│ │ ├── s_app_color_blue_2.xml
│ │ ├── s_app_color_blue_3.xml
│ │ ├── s_app_color_blue_to_red.xml
│ │ ├── s_app_color_gray.xml
│ │ ├── s_app_color_gray_dark.xml
│ │ ├── s_btn_blue.xml
│ │ ├── s_btn_gray.xml
│ │ └── s_topbar_btn_color.xml
│ ├── drawable/
│ │ ├── icon_popup_close_dark.xml
│ │ ├── icon_popup_close_with_bg_dark.xml
│ │ ├── icon_quick_action_copy.xml
│ │ ├── icon_quick_action_delete_line.xml
│ │ ├── icon_quick_action_dict.xml
│ │ ├── icon_quick_action_line.xml
│ │ ├── icon_quick_action_share.xml
│ │ ├── launcher_bg.xml
│ │ ├── pager_layout_item_bg.xml
│ │ ├── s_app_touch_fix_area_bg.xml
│ │ ├── s_list_item_bg_dark_1.xml
│ │ ├── s_list_item_bg_dark_2.xml
│ │ ├── tab_panel_bg.xml
│ │ └── web_explorer_progress.xml
│ ├── drawable-night/
│ │ └── launcher_bg.xml
│ ├── layout/
│ │ ├── activity_arch_test.xml
│ │ ├── activity_translucent.xml
│ │ ├── drawablehelper_createfromview.xml
│ │ ├── fragment_about.xml
│ │ ├── fragment_animation_listview.xml
│ │ ├── fragment_arch_test.xml
│ │ ├── fragment_button.xml
│ │ ├── fragment_collapsing_topbar_layout.xml
│ │ ├── fragment_colorhelper.xml
│ │ ├── fragment_continuous_nested_scroll.xml
│ │ ├── fragment_drawablehelper.xml
│ │ ├── fragment_emptyview.xml
│ │ ├── fragment_floatlayout.xml
│ │ ├── fragment_fsw_viewpager.xml
│ │ ├── fragment_grouplistview.xml
│ │ ├── fragment_home.xml
│ │ ├── fragment_layout.xml
│ │ ├── fragment_link_texview_layout.xml
│ │ ├── fragment_listview.xml
│ │ ├── fragment_loop_viewpager.xml
│ │ ├── fragment_notch.xml
│ │ ├── fragment_pagerlayoutmanager.xml
│ │ ├── fragment_popup.xml
│ │ ├── fragment_priority_linear_layout.xml
│ │ ├── fragment_progressbar.xml
│ │ ├── fragment_pull_horizontal_test_layout.xml
│ │ ├── fragment_pull_refresh_and_load_more_test_layout.xml
│ │ ├── fragment_pull_refresh_listview.xml
│ │ ├── fragment_pull_vertical_test_layout.xml
│ │ ├── fragment_qqface_layout.xml
│ │ ├── fragment_radius_imageview.xml
│ │ ├── fragment_radius_imageview2.xml
│ │ ├── fragment_radius_imageview2_scale_type.xml
│ │ ├── fragment_radius_imageview_scale_type.xml
│ │ ├── fragment_scheme.xml
│ │ ├── fragment_section_layout.xml
│ │ ├── fragment_slider.xml
│ │ ├── fragment_spanhelper.xml
│ │ ├── fragment_surface_test.xml
│ │ ├── fragment_swipe_delete_listview.xml
│ │ ├── fragment_tab_viewpager2_layout.xml
│ │ ├── fragment_tab_viewpager_layout.xml
│ │ ├── fragment_touch_span_fix_layout.xml
│ │ ├── fragment_verticaltextview.xml
│ │ ├── fragment_viewhelper_animation_show_and_hide.xml
│ │ ├── fragment_viewhelper_background_animation.xml
│ │ ├── fragment_webview_explorer.xml
│ │ ├── home_item_layout.xml
│ │ ├── home_layout.xml
│ │ ├── recycler_linear_layout_simple_item.xml
│ │ ├── recycler_view_item.xml
│ │ ├── simple_list_item.xml
│ │ ├── simple_list_item_1.xml
│ │ └── tipdialog_custom.xml
│ ├── values/
│ │ ├── attr.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── ids.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── theme.xml
│ └── xml/
│ └── network_security_config.xml
├── settings.gradle.kts
└── type/
├── .gitignore
├── build.gradle.kts
└── src/
└── main/
├── AndroidManifest.xml
└── java/
└── com/
└── qmuiteam/
└── qmui/
└── type/
├── EnvironmentUpdater.kt
├── Line.kt
├── LineIndentHandler.kt
├── LineLayout.kt
├── TypeEnvironment.kt
├── TypeModel.kt
├── element/
│ ├── BreakWordLineElement.kt
│ ├── DrawableElement.kt
│ ├── Element.kt
│ ├── EmojiElement.kt
│ ├── IgnoreEffectElement.kt
│ ├── NextParagraphElement.kt
│ └── TextElement.kt
├── emoji/
│ ├── EmojiModel.kt
│ └── EmojiSpan.kt
├── parser/
│ ├── EmojiResourceProvider.kt
│ ├── EmojiTextParser.kt
│ ├── ParserHelper.kt
│ ├── PlainTextParser.kt
│ └── TextParser.kt
└── view/
├── BaseTypeView.kt
├── EmojiEditText.kt
├── LineTypeView.kt
└── MarqueeTypeView.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
### 运行环境 ###
- [x] 设备型号:如:`Nexus 6`
- [x] 设备系统版本:如 `Android 5.0`
- [x] Gradle 版本:如 `2.3.0`
- [x] QMUI Android 版本:`1.x.x`
### 具体问题描述 ###
#### 问题截图 ####
#### Layout Inspector 文件([如何获取](https://github.com/QMUI/QMUI_Android/wiki/%E6%8F%90%E4%BE%9B-Layout-Inspector-%E6%96%87%E4%BB%B6)) ####
#### 异常日志(堆栈) ####
================================================
FILE: .gitignore
================================================
/*.bin
*.iml
.DS_Store
/.gradle
/.gradletasknamecache
/.idea
/bin
/build
/local.properties
/captures
/gradle/deploy.properties
================================================
FILE: CONTRIBUTING.md
================================================
[腾讯开源激励计划](https://opensource.tencent.com/contribution) 鼓励开发者的参与和贡献,期待你的加入。我们欢迎[report Issues](https://github.com/Tencent/QMUI_Android/issues) 或者 [pull requests](https://github.com/Tencent/QMUI_Android/pulls)。 在贡献代码之前请阅读以下指引。
## 问题管理
我们用 Github Issues 去跟踪 public bugs 和 feature requests。
### 使用 Issues
1. 新建 issues 前,请查找已存在或者相类似的 issue,从而保证不存在冗余。
2. 新建 issues 时,请根据我们提供的 issue 模板,尽可能提供详细的描述、截屏或者短视频来辅助我们定位问题。
### Pull Requests
我们欢迎大家为 QMUI_Android 贡献代码,在完成一个 pull request 之前请确认:
1. 从 `master` fork 你自己的分支。
2. 在修改了代码之后请修改对应的文档和注释。
3. 在新建的文件中请加入 licence 和 copy right 声明。
4. 确保一致的代码风格。
5. 做充分的测试。
================================================
FILE: LICENSE.TXT
================================================
Tencent is pleased to support the open source community by making QMUI_Android available.
Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
If you have downloaded a copy of the QMUI_Android binary from Tencent, please note that the QMUI_Android binary is licensed under the MIT License.
If you have downloaded a copy of the QMUI_Android source code from Tencent, please note that QMUI_Android source code is licensed under the MIT License, except for the third-party components listed below which are subject to different license terms. Your integration of QMUI_Android into your own projects may require compliance with the MIT License, as well as the other licenses applicable to the third-party components included within QMUI_Android.
A copy of the MIT License is included in this file.
Other dependencies and licenses:
Open Source Software Licensed Under the Apache License, Version 2.0:
----------------------------------------------------------------------------------------
1. JavaPoet 1.7.0
Copyright 2015 Square, Inc.
2. LeakCanary 1.5.4
Copyright 2015 Square, Inc.
3. Butterknife 8.8.1
Copyright 2013 Jake Wharton
Terms of the Apache License, Version 2.0:
---------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
License shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
Legal Entity shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
You (or Your) shall mean an individual or Legal Entity exercising permissions granted by this License.
Source form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
Object form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
Work shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
Derivative Works shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
Contribution shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, submitted means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution.
Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
b) You must cause any modified files to carry prominent notices stating that You changed the files; and
c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
d) If the Work includes a NOTICE text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
Terms of the MIT License:
--------------------------------------------------------------------
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# QMUI_Android
QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。
[](https://github.com/QMUI "QMUI Team")
[](http://opensource.org/licenses/MIT "Feel free to contribute.")
## 功能特性
### 全局 UI 配置
只需要修改一份配置表就可以调整 App 的全局样式,包括组件颜色、导航栏、对话框、列表等。一处修改,全局生效。
### 丰富的 UI 控件
提供丰富常用的 UI 控件,例如 BottomSheet、Tab、圆角 ImageView、下拉刷新等,使用方便灵活,并且支持自定义控件的样式。
### 高效的工具方法
提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。
## 支持 Android 版本
QMUI Android 支持 API Level 21+。
## 使用方法
可以在工程中的 qmuidemo 项目中查看各组件的使用。
## 隐私与安全
1. 框架会调用 android.os.Build 下的字段读取 brand、model 等信息,用于区分不同的设备。
2. 框架会尝试读取系统设置获取是否是全面屏手势
================================================
FILE: arch/.gitignore
================================================
/*.bin
/*.iml
/.DS_Store
/.gradletasknamecache
/.idea
/bin
/build
/local.properties
/deploy.properties
================================================
FILE: arch/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.archVer
android {
compileSdk = Dep.compileSdk
defaultConfig {
minSdk = Dep.minSdk
targetSdk = Dep.targetSdk
}
buildTypes {
getByName("release"){
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
kotlinOptions {
jvmTarget = Dep.kotlinJvmTarget
}
}
dependencies {
api(Dep.AndroidX.appcompat)
api(Dep.AndroidX.fragment)
api(project(":arch-annotation"))
compileOnly(project(":qmui"))
}
================================================
FILE: arch/proguard-rules.pro
================================================
# 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
================================================
FILE: arch/src/androidTest/java/com/qmuiteam/qmui/arch/ExampleInstrumentedTest.java
================================================
package com.qmuiteam.qmui.arch;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.qmuiteam.qmui.arch.test", appContext.getPackageName());
}
}
================================================
FILE: arch/src/main/AndroidManifest.xml
================================================
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/InnerBaseActivity.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.annotation.SuppressLint;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.LayoutInflaterCompat;
import androidx.lifecycle.Lifecycle;
import com.qmuiteam.qmui.QMUIConfig;
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord;
import com.qmuiteam.qmui.arch.record.LatestVisitArgumentCollector;
import com.qmuiteam.qmui.arch.record.RecordArgumentEditor;
import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler;
import com.qmuiteam.qmui.skin.QMUISkinLayoutInflaterFactory;
import com.qmuiteam.qmui.skin.QMUISkinManager;
import java.util.concurrent.atomic.AtomicInteger;
//Fix the bug: Only fullscreen activities can request orientation in Android version 26, 27
class InnerBaseActivity extends AppCompatActivity implements LatestVisitArgumentCollector {
private static int NO_REQUESTED_ORIENTATION_SET = -100;
private static final AtomicInteger sNextRc = new AtomicInteger(1);
private static int sLatestVisitActivityUUid = -1;
private boolean mConvertToTranslucentCauseOrientationChanged = false;
private int mPendingRequestedOrientation = NO_REQUESTED_ORIENTATION_SET;
private QMUISkinManager mSkinManager;
private final int mUUid = sNextRc.getAndIncrement();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (useQMUISkinLayoutInflaterFactory()) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater,
new QMUISkinLayoutInflaterFactory(this, layoutInflater));
}
super.onCreate(savedInstanceState);
}
void convertToTranslucentCauseOrientationChanged() {
Utils.convertActivityToTranslucent(this);
mConvertToTranslucentCauseOrientationChanged = true;
}
@Override
public void setRequestedOrientation(int requestedOrientation) {
if (mConvertToTranslucentCauseOrientationChanged && (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1)) {
Log.i("InnerBaseActivity", "setRequestedOrientation when activity is translucent");
mPendingRequestedOrientation = requestedOrientation;
} else {
super.setRequestedOrientation(requestedOrientation);
}
}
@SuppressLint("WrongConstant")
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mConvertToTranslucentCauseOrientationChanged) {
mConvertToTranslucentCauseOrientationChanged = false;
Utils.convertActivityFromTranslucent(this);
if (mPendingRequestedOrientation != NO_REQUESTED_ORIENTATION_SET) {
super.setRequestedOrientation(mPendingRequestedOrientation);
mPendingRequestedOrientation = NO_REQUESTED_ORIENTATION_SET;
}
}
}
public QMUISkinManager getSkinManager() {
return mSkinManager;
}
@Override
protected void onStart() {
super.onStart();
if (mSkinManager != null) {
mSkinManager.register(this);
}
}
@Override
protected void onStop() {
super.onStop();
if (mSkinManager != null) {
mSkinManager.unRegister(this);
}
}
@Override
protected void onResume() {
checkLatestVisitRecord();
super.onResume();
}
public final void onLatestVisitArgumentChanged() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.INITIALIZED)
&& sLatestVisitActivityUUid == mUUid) {
checkLatestVisitRecord();
}
}
protected boolean shouldPerformLatestVisitRecord() {
return true;
}
private void checkLatestVisitRecord() {
Class extends InnerBaseActivity> cls = getClass();
sLatestVisitActivityUUid = mUUid;
if (!shouldPerformLatestVisitRecord()) {
QMUILatestVisit.getInstance(this).clearActivityLatestVisitRecord();
return;
}
LatestVisitRecord latestVisitRecord = cls.getAnnotation(LatestVisitRecord.class);
if(latestVisitRecord == null || (latestVisitRecord.onlyForDebug() && !QMUIConfig.DEBUG)){
QMUILatestVisit.getInstance(this).clearActivityLatestVisitRecord();
return;
}
QMUILatestVisit.getInstance(this).performLatestVisitRecord(this);
}
@Override
public void onCollectLatestVisitArgument(RecordArgumentEditor editor) {
}
public void setSkinManager(@Nullable QMUISkinManager skinManager) {
if (mSkinManager != null) {
mSkinManager.unRegister(this);
}
mSkinManager = skinManager;
if (skinManager != null) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
skinManager.register(this);
}
}
}
public final boolean isStartedByScheme() {
return getIntent().getBooleanExtra(QMUISchemeHandler.ARG_FROM_SCHEME, false);
}
protected boolean useQMUISkinLayoutInflaterFactory() {
return true;
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUIActivity.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.WindowInsetsCompat;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.scheme.ActivitySchemeRefreshable;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.util.QMUIStatusBarHelper;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_BOTTOM_TO_TOP;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_LEFT_TO_RIGHT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_NONE;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_RIGHT_TO_LEFT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_TOP_TO_BOTTOM;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_BOTTOM;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_LEFT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_RIGHT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_TOP;
public class QMUIActivity extends InnerBaseActivity implements ActivitySchemeRefreshable {
private static final String TAG = "QMUIActivity";
private SwipeBackLayout.ListenerRemover mListenerRemover;
private SwipeBackgroundView mSwipeBackgroundView;
private boolean mIsInSwipeBack = false;
private SwipeBackLayout.SwipeListener mSwipeListener = new SwipeBackLayout.SwipeListener() {
@Override
public void onScrollStateChange(int state, float scrollPercent) {
Log.i(TAG, "SwipeListener:onScrollStateChange: state = " + state + " ;scrollPercent = " + scrollPercent);
mIsInSwipeBack = state != SwipeBackLayout.STATE_IDLE;
if (state == SwipeBackLayout.STATE_IDLE) {
if (mSwipeBackgroundView != null) {
if (scrollPercent <= 0.0F) {
mSwipeBackgroundView.unBind();
mSwipeBackgroundView = null;
} else if (scrollPercent >= 1.0F) {
// unBind mSwipeBackgroundView until onDestroy
finish();
int exitAnim = mSwipeBackgroundView.hasChildWindow() ?
R.anim.swipe_back_exit_still : R.anim.swipe_back_exit;
overridePendingTransition(R.anim.swipe_back_enter, exitAnim);
}
}
}
}
@Override
public void onScroll(int dragDirection, int movingEdge, float scrollPercent) {
if (mSwipeBackgroundView != null) {
scrollPercent = Math.max(0f, Math.min(1f, scrollPercent));
int targetOffset = (int) (Math.abs(backViewInitOffset(
QMUIActivity.this, dragDirection, movingEdge)) * (1 - scrollPercent));
SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, movingEdge, targetOffset);
}
}
@Override
public void onSwipeBackBegin(int dragDirection, int moveEdge) {
Log.i(TAG, "SwipeListener:onSwipeBackBegin: moveEdge = " + moveEdge);
onDragStart();
ViewGroup decorView = (ViewGroup) getWindow().getDecorView();
if (decorView != null) {
Activity prevActivity = QMUISwipeBackActivityManager.getInstance()
.getPenultimateActivity(QMUIActivity.this);
if(prevActivity == null){
return;
}
if (decorView.getChildAt(0) instanceof SwipeBackgroundView) {
mSwipeBackgroundView = (SwipeBackgroundView) decorView.getChildAt(0);
} else {
mSwipeBackgroundView = new SwipeBackgroundView(QMUIActivity.this, forceDisableHardwareAcceleratedForSwipeBackground());
decorView.addView(mSwipeBackgroundView, 0, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
mSwipeBackgroundView.bind(prevActivity,
QMUIActivity.this, restoreSubWindowWhenDragBack());
SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge,
Math.abs(backViewInitOffset(decorView.getContext(), dragDirection, moveEdge)));
}
}
@Override
public void onScrollOverThreshold() {
Log.i(TAG, "SwipeListener:onEdgeTouch:onScrollOverThreshold");
}
};
private SwipeBackLayout.Callback mSwipeCallback = new SwipeBackLayout.Callback() {
@Override
public int getDragDirection(SwipeBackLayout swipeBackLayout,
SwipeBackLayout.ViewMoveAction moveAction,
float downX, float downY, float dx, float dy, float touchSlop) {
if(!QMUISwipeBackActivityManager.getInstance().canSwipeBack(QMUIActivity.this)){
return SwipeBackLayout.DRAG_DIRECTION_NONE;
}
if(getIntent().getIntExtra(QMUIFragmentActivity.QMUI_MUTI_START_INDEX, 0) > 0){
return SwipeBackLayout.DRAG_DIRECTION_NONE;
}
return QMUIActivity.this.getDragDirection(swipeBackLayout,moveAction,downX, downY, dx, dy, touchSlop);
}
@Override
public void reportFrequentlyRequestLayout(int count, long duration) {
QMUIActivity.this.reportFrequentlyRequestLayout(count, duration);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
performTranslucent();
}
protected void performTranslucent(){
QMUIStatusBarHelper.translucent(this);
}
@Override
public void setContentView(View view) {
super.setContentView(newSwipeBackLayout(view));
}
@Override
public void setContentView(int layoutResID) {
SwipeBackLayout swipeBackLayout = SwipeBackLayout.wrap(this, layoutResID, dragViewMoveAction(), mSwipeCallback);
swipeBackLayout.setOnInsetsHandler(new SwipeBackLayout.OnInsetsHandler() {
@Override
public int getInsetsType() {
return getRootViewInsetsType();
}
});
mListenerRemover = swipeBackLayout.addSwipeListener(mSwipeListener);
super.setContentView(swipeBackLayout);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
super.setContentView(newSwipeBackLayout(view), params);
}
private View newSwipeBackLayout(View view) {
final SwipeBackLayout swipeBackLayout = SwipeBackLayout.wrap(view, dragViewMoveAction(), mSwipeCallback);
swipeBackLayout.setOnInsetsHandler(new SwipeBackLayout.OnInsetsHandler() {
@Override
public int getInsetsType() {
return getRootViewInsetsType();
}
});
mListenerRemover = swipeBackLayout.addSwipeListener(mSwipeListener);
return swipeBackLayout;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mListenerRemover != null) {
mListenerRemover.remove();
}
if (mSwipeBackgroundView != null) {
mSwipeBackgroundView.unBind();
mSwipeBackgroundView = null;
}
}
/**
* final this method, if need override this method, use doOnBackPressed as an alternative
*/
@Override
public final void onBackPressed() {
if (!mIsInSwipeBack) {
doOnBackPressed();
}
}
protected void doOnBackPressed() {
super.onBackPressed();
}
protected void reportFrequentlyRequestLayout(int count, long duration){
QMUILog.w(TAG, "requestLayout is too frequent(requestLayout " + count + "times within " + duration + "ms");
}
public boolean isInSwipeBack() {
return mIsInSwipeBack;
}
protected boolean forceDisableHardwareAcceleratedForSwipeBackground(){
return false;
}
/**
* disable or enable drag back
*
* @return if true open dragBack, otherwise close dragBack
* @deprecated Use {@link #getDragDirection(SwipeBackLayout, SwipeBackLayout.ViewMoveAction, float, float, float, float, float)}
*/
@Deprecated
protected boolean canDragBack() {
return true;
}
/**
* disable or enable drag back
*
* @return if true open dragBack, otherwise close dragBack
* @deprecated Use {@link #getDragDirection(SwipeBackLayout, SwipeBackLayout.ViewMoveAction, float, float, float, float, float)}
*/
@Deprecated
protected boolean canDragBack(Context context, int dragDirection, int moveEdge) {
return canDragBack();
}
/**
* @return the init offset for backView for Parallax scrolling
* @deprecated Use {@link #backViewInitOffset(Context, int, int)}
*/
@Deprecated
protected int backViewInitOffset() {
return 0;
}
protected int backViewInitOffset(Context context, int dragDirection, int moveEdge) {
return backViewInitOffset();
}
protected int getDragDirection(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull SwipeBackLayout.ViewMoveAction viewMoveAction,
float downX, float downY, float dx, float dy, float slopTouch){
int targetDirection = dragBackDirection();
if(!canDragBack(swipeBackLayout.getContext(), targetDirection, viewMoveAction.getEdge(targetDirection))){
return DRAG_DIRECTION_NONE;
}
int edgeSize = QMUIDisplayHelper.dp2px(swipeBackLayout.getContext(), 20);
if (targetDirection == DRAG_DIRECTION_LEFT_TO_RIGHT) {
if(downX < edgeSize && dx >= slopTouch){
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_RIGHT_TO_LEFT) {
if(downX > swipeBackLayout.getWidth() - edgeSize && -dx >= slopTouch){
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_TOP_TO_BOTTOM) {
if(downY < edgeSize && dy >= slopTouch){
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_BOTTOM_TO_TOP) {
if(downY > swipeBackLayout.getHeight() - edgeSize && -dy >= slopTouch){
return targetDirection;
}
}
return DRAG_DIRECTION_NONE;
}
/**
* called when drag back started.
*/
protected void onDragStart() {
}
/**
* @return
* @deprecated Use {@link #dragBackDirection()}
*/
@Deprecated
protected int dragBackEdge() {
return EDGE_LEFT;
}
protected int dragBackDirection() {
int oldEdge = dragBackEdge();
if (oldEdge == EDGE_RIGHT) {
return SwipeBackLayout.DRAG_DIRECTION_RIGHT_TO_LEFT;
} else if (oldEdge == EDGE_TOP) {
return SwipeBackLayout.DRAG_DIRECTION_TOP_TO_BOTTOM;
} else if (oldEdge == EDGE_BOTTOM) {
return SwipeBackLayout.DRAG_DIRECTION_BOTTOM_TO_TOP;
}
return SwipeBackLayout.DRAG_DIRECTION_LEFT_TO_RIGHT;
}
protected SwipeBackLayout.ViewMoveAction dragViewMoveAction() {
return SwipeBackLayout.MOVE_VIEW_AUTO;
}
/**
* restore sub window(e.g dialog) when drag back to previous activity
*
* @return
*/
protected boolean restoreSubWindowWhenDragBack() {
return true;
}
/**
* When finishing last activity, let activity have a chance to start a new Activity
*
* @return Intent to start a new Activity
*/
public Intent onLastActivityFinish() {
return null;
}
@WindowInsetsCompat.Type.InsetsType
public int getRootViewInsetsType() {
return WindowInsetsCompat.Type.ime();
}
@Override
public void finish() {
if (isTaskRoot()) {
Intent intent = onLastActivityFinish();
if (intent != null) {
startActivity(intent);
}
}
super.finish();
}
@Override
public void refreshFromScheme(@Nullable Intent intent) {
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragment.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_BOTTOM_TO_TOP;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_LEFT_TO_RIGHT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_NONE;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_RIGHT_TO_LEFT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.DRAG_DIRECTION_TOP_TO_BOTTOM;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_BOTTOM;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_LEFT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_RIGHT;
import static com.qmuiteam.qmui.arch.SwipeBackLayout.EDGE_TOP;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import com.qmuiteam.qmui.QMUIConfig;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord;
import com.qmuiteam.qmui.arch.effect.Effect;
import com.qmuiteam.qmui.arch.effect.FragmentResultEffect;
import com.qmuiteam.qmui.arch.effect.QMUIFragmentEffectHandler;
import com.qmuiteam.qmui.arch.effect.QMUIFragmentEffectRegistration;
import com.qmuiteam.qmui.arch.effect.QMUIFragmentEffectRegistry;
import com.qmuiteam.qmui.arch.effect.QMUIFragmentResultEffectHandler;
import com.qmuiteam.qmui.arch.record.LatestVisitArgumentCollector;
import com.qmuiteam.qmui.arch.record.RecordArgumentEditor;
import com.qmuiteam.qmui.arch.scheme.FragmentSchemeRefreshable;
import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler;
import com.qmuiteam.qmui.util.QMUIDisplayHelper;
import com.qmuiteam.qmui.util.QMUIKeyboardHelper;
import com.qmuiteam.qmui.widget.QMUITopBar;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* With the use of {@link QMUIFragmentActivity}, {@link QMUIFragment} brings more features,
* such as swipe back, transition config, and so on.
*
* Created by cgspine on 15/9/14.
*/
public abstract class QMUIFragment extends Fragment implements
LatestVisitArgumentCollector,
FragmentSchemeRefreshable{
static final String SWIPE_BACK_VIEW = "swipe_back_view";
private static final String TAG = QMUIFragment.class.getSimpleName();
private static final String QMUI_DISABLE_SWIPE_BACK_KEY = "qmui_disable_swipe_back";
public static final TransitionConfig SLIDE_TRANSITION_CONFIG = new TransitionConfig(
R.animator.slide_in_right, R.animator.slide_out_left,
R.animator.slide_in_left, R.animator.slide_out_right,
R.anim.slide_in_left, R.anim.slide_out_right
);
public static final TransitionConfig SCALE_TRANSITION_CONFIG = new TransitionConfig(
R.animator.scale_enter, R.animator.slide_still,
R.animator.slide_still, R.animator.scale_exit,
R.anim.slide_still, R.anim.scale_exit
);
public static final int RESULT_CANCELED = Activity.RESULT_CANCELED;
public static final int RESULT_OK = Activity.RESULT_OK;
public static final int RESULT_FIRST_USER = Activity.RESULT_FIRST_USER;
public static final int ANIMATION_ENTER_STATUS_NOT_START = -1;
public static final int ANIMATION_ENTER_STATUS_STARTED = 0;
public static final int ANIMATION_ENTER_STATUS_END = 1;
private static boolean sPopBackWhenSwipeFinished = false;
private static final int NO_REQUEST_CODE = 0;
private static final AtomicInteger sNextRc = new AtomicInteger(1);
private static int sLatestVisitFragmentUUid = -1;
private int mSourceRequestCode = NO_REQUEST_CODE;
private final int mUUid = sNextRc.getAndIncrement();
private int mTargetFragmentUUid = -1;
private int mTargetRequestCode = NO_REQUEST_CODE;
private View mBaseView;
private View mCacheRootView;
private SwipeBackLayout mCacheSwipeBackView;
private boolean isCreateForSwipeBack = false;
private SwipeBackLayout.ListenerRemover mListenerRemover;
private SwipeBackgroundView mSwipeBackgroundView;
private boolean mIsInSwipeBack = false;
private boolean mFinishActivityIfOnBackPressed = false;
boolean mDisableSwipeBackByMutiStarted = false;
private int mEnterAnimationStatus = ANIMATION_ENTER_STATUS_NOT_START;
private MutableLiveData isInEnterAnimationLiveData = new MutableLiveData<>(false);
private boolean mCalled = true;
private ArrayList mDelayRenderRunnableList;
private ArrayList mPostResumeRunnableList;
private Runnable mCheckPostResumeRunnable = new Runnable() {
@Override
public void run() {
if (isResumed() && mPostResumeRunnableList != null) {
ArrayList list = mPostResumeRunnableList;
if (!list.isEmpty()) {
for (Runnable runnable : list) {
runnable.run();
}
}
mPostResumeRunnableList = null;
}
}
};
private QMUIFragmentEffectRegistry mFragmentEffectRegistry;
private OnBackPressedDispatcher mOnBackPressedDispatcher;
private OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (sPopBackWhenSwipeFinished) {
// must use normal back procedure when swipe finished.
onNormalBackPressed();
return;
}
QMUIFragment.this.onBackPressed();
}
};
public QMUIFragment() {
super();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mOnBackPressedDispatcher = requireActivity().getOnBackPressedDispatcher();
mOnBackPressedDispatcher.addCallback(this, mOnBackPressedCallback);
registerEffect(this, new QMUIFragmentResultEffectHandler() {
@Override
public boolean shouldHandleEffect(@NonNull FragmentResultEffect effect) {
return effect.getRequestCode() == mSourceRequestCode && effect.getRequestFragmentUUid() == mUUid;
}
@Override
public void handleEffect(@NonNull FragmentResultEffect effect) {
onFragmentResult(effect.getRequestCode(), effect.getResultCode(), effect.getIntent());
mSourceRequestCode = NO_REQUEST_CODE;
}
@Override
public void handleEffect(@NonNull List effects) {
// only handle the latest
handleEffect(effects.get(effects.size() - 1));
}
});
}
public final QMUIFragmentActivity getBaseFragmentActivity() {
return (QMUIFragmentActivity) getActivity();
}
public boolean isAttachedToActivity() {
return !isRemoving() && mBaseView != null;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(QMUI_DISABLE_SWIPE_BACK_KEY, mDisableSwipeBackByMutiStarted);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (mListenerRemover != null) {
mListenerRemover.remove();
mListenerRemover = null;
}
if(getParentFragment() == null && mCacheRootView != null && mCacheRootView.getParent() instanceof ViewGroup){
((ViewGroup) mCacheRootView.getParent()).removeView(mCacheRootView);
}
mBaseView = null;
mEnterAnimationStatus = ANIMATION_ENTER_STATUS_NOT_START;
}
@Override
public void onResume() {
if(mEnterAnimationStatus != ANIMATION_ENTER_STATUS_END){
mEnterAnimationStatus = ANIMATION_ENTER_STATUS_END;
notifyDelayRenderRunnableList();
}
checkLatestVisitRecord();
checkForRequestForHandlePopBack();
super.onResume();
if (mBaseView != null && mPostResumeRunnableList != null && !mPostResumeRunnableList.isEmpty()) {
mBaseView.post(mCheckPostResumeRunnable);
}
}
protected void checkForRequestForHandlePopBack(){
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if(provider != null){
provider.requestForHandlePopBack(false);
}
}
protected boolean shouldCheckLatestVisitRecord(){
return getParentFragment() == null || (getParentFragment() instanceof QMUINavFragment);
}
protected boolean shouldPerformLatestVisitRecord() {
return true;
}
private void checkLatestVisitRecord() {
if(!shouldCheckLatestVisitRecord()){
return;
}
Activity activity = getActivity();
if (!(activity instanceof QMUIFragmentActivity)) {
return;
}
if (this instanceof QMUINavFragment) {
return;
}
sLatestVisitFragmentUUid = mUUid;
if (!shouldPerformLatestVisitRecord()) {
QMUILatestVisit.getInstance(getContext()).clearFragmentLatestVisitRecord();
return;
}
Class extends QMUIFragment> cls = getClass();
LatestVisitRecord latestVisitRecord = cls.getAnnotation(LatestVisitRecord.class);
if (latestVisitRecord == null || (latestVisitRecord.onlyForDebug() && !QMUIConfig.DEBUG)) {
QMUILatestVisit.getInstance(getContext()).clearFragmentLatestVisitRecord();
return;
}
if (!activity.getClass().isAnnotationPresent(LatestVisitRecord.class)) {
throw new RuntimeException(String.format("Can not perform LatestVisitRecord, " +
"%s must be annotated by LatestVisitRecord", activity.getClass().getSimpleName()));
}
QMUILatestVisit.getInstance(getContext()).performLatestVisitRecord(this);
}
public final void onLatestVisitArgumentChanged() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.INITIALIZED) && sLatestVisitFragmentUUid == mUUid) {
checkLatestVisitRecord();
}
}
@Override
public void onCollectLatestVisitArgument(RecordArgumentEditor editor) {
}
@Nullable
public QMUIFragmentEffectRegistration registerEffect(
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final QMUIFragmentEffectHandler effectHandler) {
FragmentActivity activity = getActivity();
if (activity == null) {
throw new RuntimeException("Fragment(" + getClass().getSimpleName() + ") not attached to Activity.");
}
ensureFragmentEffectRegistry();
return mFragmentEffectRegistry.register(lifecycleOwner, effectHandler);
}
public void notifyEffect(T effect) {
FragmentActivity activity = getActivity();
if (activity == null) {
QMUILog.d(TAG, "Fragment(" + getClass().getSimpleName() + ") not attached to Activity.");
return;
}
ensureFragmentEffectRegistry();
mFragmentEffectRegistry.notifyEffect(effect);
}
private void ensureFragmentEffectRegistry() {
if (mFragmentEffectRegistry == null) {
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
ViewModelStoreOwner viewModelStoreOwner = provider != null ? provider.getContainerViewModelStoreOwner() : requireActivity();
mFragmentEffectRegistry = new ViewModelProvider(viewModelStoreOwner).get(QMUIFragmentEffectRegistry.class);
}
}
@Nullable
protected QMUIFragmentContainerProvider findFragmentContainerProvider(boolean includeSelf) {
Fragment current = includeSelf ? this : getParentFragment();
while (current != null) {
if (current instanceof QMUIFragmentContainerProvider) {
return (QMUIFragmentContainerProvider) current;
} else {
current = current.getParentFragment();
}
}
Activity activity = getActivity();
if (activity instanceof QMUIFragmentContainerProvider) {
return (QMUIFragmentContainerProvider) activity;
}
return null;
}
public int startFragmentAndDestroyCurrent(QMUIFragment fragment) {
return startFragmentAndDestroyCurrent(fragment, true);
}
/**
* start a new fragment and then destroy current fragment.
* assume there is a fragment stack(A->B->C), and you use this method to start a new
* fragment D and destroy fragment C. Now you are in fragment D, if you want call
* {@link #popBackStack()} to back to B, what the animation should be? Sometimes we hope run
* animation generated by transition B->C, but sometimes we hope run animation generated by
* transition C->D. this why second parameter exists.
*
* @param fragment new fragment to start
* @param useNewTransitionConfigWhenPop if true, use animation generated by transition C->D,
* else, use animation generated by transition B->C
*/
public int startFragmentAndDestroyCurrent(QMUIFragment fragment,
boolean useNewTransitionConfigWhenPop) {
if (!checkStateLoss("startFragmentAndDestroyCurrent")) {
return -1;
}
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true);
if (provider == null) {
if (QMUIConfig.DEBUG) {
throw new RuntimeException("Can not find the fragment container provider.");
} else {
Log.d(TAG, "Can not find the fragment container provider.");
return -1;
}
}
if(provider.getContainerFragmentManager().isDestroyed()){
return -1;
}
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
String tagName = fragment.getClass().getSimpleName();
FragmentManager fragmentManager = provider.getContainerFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction()
.setCustomAnimations(
transitionConfig.enter, transitionConfig.exit,
transitionConfig.popenter, transitionConfig.popout)
.setPrimaryNavigationFragment(null)
.replace(provider.getContextViewId(), fragment, tagName);
int index = transaction.commit();
Utils.modifyOpForStartFragmentAndDestroyCurrent(fragmentManager, fragment, useNewTransitionConfigWhenPop, transitionConfig);
return index;
}
/**
* start a new fragment and add to BackStack
* @param fragment the fragment to start
* @return Returns the identifier of this transaction's back stack entry,
* if {@link FragmentTransaction#addToBackStack(String)} had been called. Otherwise, returns
* a negative number.
*/
public int startFragment(QMUIFragment fragment) {
if (!checkStateLoss("startFragment")) {
return -1;
}
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true);
if (provider == null) {
if (QMUIConfig.DEBUG) {
throw new RuntimeException("Can not find the fragment container provider.");
} else {
Log.d(TAG, "Can not find the fragment container provider.");
return -1;
}
}
return startFragment(fragment, provider);
}
public int startFragment(QMUIFragment... fragments){
if (!checkStateLoss("startFragment")) {
return -1;
}
if(fragments.length == 0){
return -1;
}
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true);
if (provider == null) {
if (QMUIConfig.DEBUG) {
throw new RuntimeException("Can not find the fragment container provider.");
} else {
Log.d(TAG, "Can not find the fragment container provider.");
return -1;
}
}
if(provider.getContainerFragmentManager().isDestroyed()){
return -1;
}
if(fragments.length == 1){
return startFragment(fragments[0], provider);
}
ArrayList transactions = new ArrayList<>();
TransitionConfig lastTransitionConfig = fragments[fragments.length - 1].onFetchTransitionConfig();
boolean disableSwipeBack = false;
for (QMUIFragment fragment : fragments) {
FragmentTransaction transaction = provider.getContainerFragmentManager()
.beginTransaction()
.setPrimaryNavigationFragment(null);
TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
if(disableSwipeBack){
fragment.mDisableSwipeBackByMutiStarted = true;
}
disableSwipeBack = true;
String tagName = fragment.getClass().getSimpleName();
transaction.setCustomAnimations(transitionConfig.enter, lastTransitionConfig.exit, transitionConfig.popenter, transitionConfig.popout);
transaction.replace(provider.getContextViewId(), fragment, tagName);
transaction.addToBackStack(tagName);
transactions.add(transaction);
transaction.setReorderingAllowed(true);
}
for(FragmentTransaction transaction: transactions){
transaction.commit();
}
return 0;
}
/**
* simulate the behavior of startActivityForResult/onActivityResult:
* 1. Jump fragment1 to fragment2 via startActivityForResult(fragment2, requestCode)
* 2. Pass data from fragment2 to fragment1 via setFragmentResult(RESULT_OK, data)
* 3. Get data in fragment1 through onFragmentResult(requestCode, resultCode, data)
*
* @deprecated use {@link #registerEffect} for a replacement
*
* @param fragment target fragment
* @param requestCode request code
*/
@Deprecated
public int startFragmentForResult(QMUIFragment fragment, int requestCode) {
if (!checkStateLoss("startFragmentForResult")) {
return -1;
}
if (requestCode == NO_REQUEST_CODE) {
throw new RuntimeException("requestCode can not be " + NO_REQUEST_CODE);
}
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(true);
if (provider == null) {
if (QMUIConfig.DEBUG) {
throw new RuntimeException("Can not find the fragment container provider.");
} else {
Log.d(TAG, "Can not find the fragment container provider.");
return -1;
}
}
mSourceRequestCode = requestCode;
fragment.mTargetFragmentUUid = mUUid;
fragment.mTargetRequestCode = requestCode;
return startFragment(fragment, provider);
}
private int startFragment(QMUIFragment fragment, QMUIFragmentContainerProvider provider) {
if(provider.getContainerFragmentManager().isDestroyed()){
return -1;
}
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
String tagName = fragment.getClass().getSimpleName();
return provider.getContainerFragmentManager()
.beginTransaction()
.setPrimaryNavigationFragment(null)
.setCustomAnimations(transitionConfig.enter, transitionConfig.exit, transitionConfig.popenter, transitionConfig.popout)
.replace(provider.getContextViewId(), fragment, tagName)
.addToBackStack(tagName)
.commit();
}
/**
*
* @param resultCode
* @param data
*
* @deprecated use {@link #notifyEffect} for a replacement
*/
@Deprecated
public void setFragmentResult(int resultCode, Intent data) {
if (mTargetRequestCode == NO_REQUEST_CODE) {
return;
}
notifyEffect(new FragmentResultEffect(mTargetFragmentUUid, resultCode, mTargetRequestCode, data));
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null){
mDisableSwipeBackByMutiStarted = savedInstanceState.getBoolean(QMUI_DISABLE_SWIPE_BACK_KEY, false);
}
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mBaseView.getTag(R.id.qmui_arch_reused_layout) == null) {
onViewCreated(mBaseView);
mBaseView.setTag(R.id.qmui_arch_reused_layout, true);
}
}
private SwipeBackLayout newSwipeBackLayout() {
if(mCacheSwipeBackView != null && getParentFragment() != null){
if (mCacheSwipeBackView.getParent() != null) {
((ViewGroup) mCacheSwipeBackView.getParent()).removeView(mCacheSwipeBackView);
}
if(mCacheSwipeBackView.getParent() == null){
initSwipeBackLayout(mCacheSwipeBackView);
return mCacheSwipeBackView;
}
}
View rootView = mCacheRootView;
if (rootView == null) {
rootView = onCreateView();
mCacheRootView = rootView;
} else {
if (rootView.getParent() != null) {
((ViewGroup) rootView.getParent()).removeView(rootView);
}
}
SwipeBackLayout swipeBackLayout = SwipeBackLayout.wrap(rootView,
dragViewMoveAction(),
new SwipeBackLayout.Callback() {
@Override
public int getDragDirection(SwipeBackLayout swipeBackLayout, SwipeBackLayout.ViewMoveAction viewMoveAction, float downX, float downY, float dx, float dy, float touchSlop) {
mCalled = false;
if(mDisableSwipeBackByMutiStarted){
return DRAG_DIRECTION_NONE;
}
boolean canHandle = canHandleSwipeBack();
if (canHandle && !mCalled) {
throw new RuntimeException(getClass().getSimpleName() + " did not call through to super.shouldPreventSwipeBack()");
}
if(!canHandle){
return DRAG_DIRECTION_NONE;
}
return QMUIFragment.this.getDragDirection(
swipeBackLayout, viewMoveAction, downX, downY, dx, dy, touchSlop);
}
@Override
public void reportFrequentlyRequestLayout(int count, long duration) {
QMUIFragment.this.reportFrequentlyRequestLayout(count, duration);
}
});
initSwipeBackLayout(swipeBackLayout);
if(getParentFragment() != null){
mCacheSwipeBackView = swipeBackLayout;
}
return swipeBackLayout;
}
private void initSwipeBackLayout(SwipeBackLayout swipeBackLayout){
if(mListenerRemover != null){
mListenerRemover.remove();
}
mListenerRemover = swipeBackLayout.addSwipeListener(mSwipeListener);
swipeBackLayout.setOnInsetsHandler(new SwipeBackLayout.OnInsetsHandler() {
@Override
public int getInsetsType() {
return getRootViewInsetsType();
}
});
if (isCreateForSwipeBack) {
swipeBackLayout.setTag(R.id.fragment_container_view_tag, this);
}
}
private SwipeBackLayout.SwipeListener mSwipeListener = new SwipeBackLayout.SwipeListener() {
private QMUIFragment mModifiedFragment = null;
@Override
public void onScrollStateChange(int state, float scrollPercent) {
Log.i(TAG, "SwipeListener:onScrollStateChange: state = " + state + " ;scrollPercent = " + scrollPercent);
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if (provider == null || provider.getFragmentContainerView() == null) {
return;
}
FragmentContainerView container = provider.getFragmentContainerView();
mIsInSwipeBack = state != SwipeBackLayout.STATE_IDLE;
if (state == SwipeBackLayout.STATE_IDLE) {
if (mSwipeBackgroundView != null) {
if (scrollPercent <= 0.0F) {
mSwipeBackgroundView.unBind();
mSwipeBackgroundView = null;
} else if (scrollPercent >= 1.0F) {
// unbind mSwipeBackgroundView util onDestroy
Activity activity = getActivity();
if (activity != null) {
sPopBackWhenSwipeFinished = true;
// must call before popBackStack. mSwipeBackgroundView maybe released in popBackStack
int exitAnim = mSwipeBackgroundView.hasChildWindow() ?
R.anim.swipe_back_exit_still : R.anim.swipe_back_exit;
popBackStack();
activity.overridePendingTransition(R.anim.swipe_back_enter, exitAnim);
sPopBackWhenSwipeFinished = false;
}
}
return;
}
if (scrollPercent <= 0.0F) {
handleSwipeBackCancelOrFinished(container);
} else if (scrollPercent >= 1.0F) {
handleSwipeBackCancelOrFinished(container);
FragmentManager fragmentManager = provider.getContainerFragmentManager();
Utils.findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() {
@Override
public boolean handle(Object op) {
Field cmdField = Utils.getOpCmdField(op);
if (cmdField == null) {
return false;
}
try {
cmdField.setAccessible(true);
int cmd = (int) cmdField.get(op);
if (cmd == 1) {
Field popEnterAnimField = Utils.getOpPopExitAnimField(op);
if (popEnterAnimField != null) {
popEnterAnimField.setAccessible(true);
popEnterAnimField.set(op, 0);
}
} else if (cmd == 3) {
Field popExitAnimField = Utils.getOpPopEnterAnimField(op);
if (popExitAnimField != null) {
popExitAnimField.setAccessible(true);
popExitAnimField.set(op, 0);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean needReNameTag() {
return false;
}
@Override
public String newTagName() {
return null;
}
});
sPopBackWhenSwipeFinished = true;
popBackStack();
sPopBackWhenSwipeFinished = false;
}
}
}
@Override
public void onScroll(int dragDirection, int moveEdge, float scrollPercent) {
scrollPercent = Math.max(0f, Math.min(1f, scrollPercent));
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if (provider == null || provider.getFragmentContainerView() == null) {
return;
}
FragmentContainerView container = provider.getFragmentContainerView();
int targetOffset = (int) (Math.abs(
backViewInitOffset(container.getContext(), dragDirection, moveEdge)) * (1 - scrollPercent));
int childCount = container.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
View view = container.getChildAt(i);
Object tag = view.getTag(R.id.qmui_arch_swipe_layout_in_back);
if (SWIPE_BACK_VIEW.equals(tag)) {
SwipeBackLayout.translateInSwipeBack(view, moveEdge, targetOffset);
}
}
if (mSwipeBackgroundView != null) {
SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge, targetOffset);
}
}
@SuppressLint("PrivateApi")
@Override
public void onSwipeBackBegin(final int dragDirection, final int moveEdge) {
Log.i(TAG, "SwipeListener:onSwipeBackBegin: moveEdge = " + moveEdge);
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if (provider == null || provider.getFragmentContainerView() == null) {
return;
}
final FragmentContainerView container = provider.getFragmentContainerView();
QMUIKeyboardHelper.hideKeyboard(mBaseView);
onDragStart();
FragmentManager fragmentManager = provider.getContainerFragmentManager();
int backStackCount = fragmentManager.getBackStackEntryCount();
if (backStackCount > 1 && !mFinishActivityIfOnBackPressed) {
Utils.findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() {
@Override
public boolean handle(Object op) {
Field cmdField = Utils.getOpCmdField(op);
if (cmdField == null) {
return false;
}
try {
cmdField.setAccessible(true);
int cmd = (int) cmdField.get(op);
if (cmd == 3) {
Field fragmentField = Utils.getOpFragmentField(op);
if (fragmentField != null) {
fragmentField.setAccessible(true);
Object fragmentObject = fragmentField.get(op);
if (fragmentObject instanceof QMUIFragment) {
mModifiedFragment = (QMUIFragment) fragmentObject;
mModifiedFragment.isCreateForSwipeBack = true;
View baseView = mModifiedFragment.onCreateView(LayoutInflater.from(getContext()), container, null);
mModifiedFragment.isCreateForSwipeBack = false;
if (baseView != null) {
addViewInSwipeBack(container, baseView, 0);
handleChildFragmentListWhenSwipeBackStart(mModifiedFragment, baseView);
SwipeBackLayout.translateInSwipeBack(baseView, moveEdge,
Math.abs(backViewInitOffset(baseView.getContext(), dragDirection, moveEdge)));
}
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean needReNameTag() {
return false;
}
@Override
public String newTagName() {
return null;
}
});
} else if (getParentFragment() == null) {
Activity currentActivity = getActivity();
if (currentActivity != null) {
ViewGroup decorView = (ViewGroup) currentActivity.getWindow().getDecorView();
Activity prevActivity = QMUISwipeBackActivityManager.getInstance()
.getPenultimateActivity(currentActivity);
if(prevActivity == null){
return;
}
if (decorView.getChildAt(0) instanceof SwipeBackgroundView) {
mSwipeBackgroundView = (SwipeBackgroundView) decorView.getChildAt(0);
} else {
mSwipeBackgroundView = new SwipeBackgroundView(getContext(), forceDisableHardwareAcceleratedForSwipeBackground());
decorView.addView(mSwipeBackgroundView, 0, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
mSwipeBackgroundView.bind(prevActivity, currentActivity, restoreSubWindowWhenDragBack());
SwipeBackLayout.translateInSwipeBack(mSwipeBackgroundView, moveEdge,
Math.abs(backViewInitOffset(decorView.getContext(), dragDirection, moveEdge)));
}
}
}
@Override
public void onScrollOverThreshold() {
Log.i(TAG, "SwipeListener:onEdgeTouch:onScrollOverThreshold");
}
private void addViewInSwipeBack(ViewGroup parent, View child) {
addViewInSwipeBack(parent, child, -1);
}
private void addViewInSwipeBack(ViewGroup parent, View child, int index) {
if (parent != null && child != null) {
child.setTag(R.id.qmui_arch_swipe_layout_in_back, SWIPE_BACK_VIEW);
parent.addView(child, index);
}
}
private void removeViewInSwipeBack(ViewGroup parent, Function onRemove) {
if (parent != null) {
int childCount = parent.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
View view = parent.getChildAt(i);
Object tag = view.getTag(R.id.qmui_arch_swipe_layout_in_back);
if (SWIPE_BACK_VIEW.equals(tag)) {
if (onRemove != null) {
onRemove.apply(view);
}
view.setTranslationY(0);
view.setTranslationX(0);
parent.removeView(view);
}
}
}
}
private void handleChildFragmentListWhenSwipeBackStart(Fragment parentFragment, View baseView) throws IllegalAccessException {
// handle issue #235
if (baseView instanceof ViewGroup) {
ViewGroup childMainContainer = (ViewGroup) baseView;
FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
List childFragmentList = childFragmentManager.getFragments();
int childContainerId = 0;
ViewGroup childContainer = null;
for (Fragment fragment : childFragmentList) {
if (fragment instanceof QMUIFragment) {
QMUIFragment qmuiFragment = (QMUIFragment) fragment;
Field containerIdField = null;
try {
containerIdField = Fragment.class.getDeclaredField("mContainerId");
} catch (NoSuchFieldException e) {
continue;
}
containerIdField.setAccessible(true);
int containerId = containerIdField.getInt(qmuiFragment);
if (containerId != 0) {
if (childContainerId != containerId) {
childContainerId = containerId;
childContainer = childMainContainer.findViewById(containerId);
}
if (childContainer != null) {
qmuiFragment.isCreateForSwipeBack = true;
View childView = fragment.onCreateView(
LayoutInflater.from(childContainer.getContext()), childContainer, null);
qmuiFragment.isCreateForSwipeBack = false;
addViewInSwipeBack(childContainer, childView);
handleChildFragmentListWhenSwipeBackStart(fragment, childView);
}
}
}
}
}
}
private void handleSwipeBackCancelOrFinished(ViewGroup container) {
removeViewInSwipeBack(container, new Function() {
@Override
public Void apply(View input) {
if (mModifiedFragment == null) {
return null;
}
if (input instanceof ViewGroup) {
ViewGroup childMainContainer = (ViewGroup) input;
FragmentManager childFragmentManager = mModifiedFragment.getChildFragmentManager();
List childFragmentList = childFragmentManager.getFragments();
int childContainerId = 0;
try {
for (Fragment fragment : childFragmentList) {
if (fragment instanceof QMUIFragment) {
QMUIFragment qmuiFragment = (QMUIFragment) fragment;
Field containerIdField = Fragment.class.getDeclaredField("mContainerId");
containerIdField.setAccessible(true);
int containerId = containerIdField.getInt(qmuiFragment);
if (containerId != 0 && childContainerId != containerId) {
childContainerId = containerId;
ViewGroup childContainer = childMainContainer.findViewById(containerId);
removeViewInSwipeBack(childContainer, null);
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
return null;
}
});
mModifiedFragment = null;
}
};
public boolean isInSwipeBack() {
return mIsInSwipeBack;
}
protected boolean forceDisableHardwareAcceleratedForSwipeBackground(){
return false;
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
SwipeBackLayout swipeBackLayout = newSwipeBackLayout();
if (!isCreateForSwipeBack) {
mBaseView = swipeBackLayout.getContentView();
swipeBackLayout.setTag(R.id.qmui_arch_swipe_layout_in_back, null);
}
swipeBackLayout.setFitsSystemWindows(false);
return swipeBackLayout;
}
private void bubbleBackPressedEvent() {
// disable this and go with FragmentManager's backPressesCallback
// because it will call execPendingActions before popBackStackImmediate
mOnBackPressedCallback.setEnabled(false);
mOnBackPressedDispatcher.onBackPressed();
mOnBackPressedCallback.setEnabled(true);
}
protected final void onNormalBackPressed() {
runSideEffectOnNormalBackPressed();
if (getParentFragment() != null) {
bubbleBackPressedEvent();
return;
}
FragmentActivity activity = requireActivity();
if (activity instanceof QMUIFragmentContainerProvider) {
QMUIFragmentContainerProvider provider = (QMUIFragmentContainerProvider) activity;
if ((provider.getContainerFragmentManager().getBackStackEntryCount() > 1 && !mFinishActivityIfOnBackPressed) || provider.getContainerFragmentManager().getPrimaryNavigationFragment() == this) {
bubbleBackPressedEvent();
} else {
QMUIFragment.TransitionConfig transitionConfig = onFetchTransitionConfig();
if (needInterceptLastFragmentFinish()) {
if(!sPopBackWhenSwipeFinished){
activity.finishAfterTransition();
}else{
activity.finish();
}
activity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation);
return;
}
Object toExec = onLastFragmentFinish();
if (toExec != null) {
if (toExec instanceof QMUIFragment) {
QMUIFragment fragment = (QMUIFragment) toExec;
startFragmentAndDestroyCurrent(fragment, false);
} else if (toExec instanceof Intent) {
Intent intent = (Intent) toExec;
startActivity(intent);
activity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation);
activity.finish();
} else {
onHandleSpecLastFragmentFinish(activity, transitionConfig, toExec);
}
} else {
if(!sPopBackWhenSwipeFinished){
activity.finishAfterTransition();
}else{
activity.finish();
}
activity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation);
}
}
} else {
bubbleBackPressedEvent();
}
}
protected void runSideEffectOnNormalBackPressed() {
}
protected void onBackPressed() {
onNormalBackPressed();
}
protected void reportFrequentlyRequestLayout(int count, long duration){
QMUILog.w(TAG, "requestLayout is too frequent(requestLayout " + count + "times within " + duration + "ms");
}
protected void onHandleSpecLastFragmentFinish(FragmentActivity fragmentActivity,
QMUIFragment.TransitionConfig transitionConfig,
Object toExec) {
fragmentActivity.finish();
fragmentActivity.overridePendingTransition(transitionConfig.popenterAnimation, transitionConfig.popoutAnimation);
}
/**
* pop back
*/
protected void popBackStack() {
if (mOnBackPressedDispatcher != null) {
mOnBackPressedDispatcher.onBackPressed();
}
}
/**
* pop back to a clazz type fragment
*
* Assuming there is a back stack: Home -> List -> Detail. Perform popBackStack(Home.class),
* Home is the current fragment
*
* if the clazz type fragment doest not exist in back stack, this method is Equivalent
* to popBackStack()
*
* @param cls the type of target fragment
*/
protected void popBackStack(Class extends QMUIFragment> cls) {
if (checkPopBack()) {
getParentFragmentManager().popBackStack(cls.getSimpleName(), 0);
}
}
/**
* pop back to a non-class type Fragment
*
* @param cls the target fragment class type
*/
protected void popBackStackInclusive(Class extends QMUIFragment> cls) {
if (checkPopBack()) {
getParentFragmentManager().popBackStack(cls.getSimpleName(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
private boolean checkPopBack() {
if (!isResumed() || mEnterAnimationStatus != ANIMATION_ENTER_STATUS_END) {
return false;
}
return checkStateLoss("popBackStack");
}
protected void popBackStackAfterResume() {
if (isResumed() && mEnterAnimationStatus == ANIMATION_ENTER_STATUS_END) {
popBackStack();
} else {
runAfterAnimation(new Runnable() {
@Override
public void run() {
if (isResumed()) {
popBackStack();
} else {
runAfterResumed(new Runnable() {
@Override
public void run() {
popBackStack();
}
});
}
}
}, true);
}
}
private boolean checkStateLoss(String logName) {
if (!isAdded()) {
return false;
}
FragmentManager fragmentManager = getParentFragmentManager();
if (fragmentManager.isStateSaved()) {
QMUILog.d(TAG, logName + " can not be invoked after onSaveInstanceState");
return false;
}
return true;
}
@Nullable
@Override
public final Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
return null;
}
@Nullable
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
if(enter && nextAnim != 0){
Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
checkAndCallOnEnterAnimationStart(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
checkAndCallOnEnterAnimationEnd(animation);
}
});
return animator;
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
private void checkAndCallOnEnterAnimationStart(@Nullable Animator animation) {
mCalled = false;
onEnterAnimationStart(animation);
if (!mCalled) {
throw new RuntimeException(getClass().getSimpleName() + " did not call through to super.onEnterAnimationStart(Animation)");
}
}
private void checkAndCallOnEnterAnimationEnd(@Nullable Animator animation) {
mCalled = false;
onEnterAnimationEnd(animation);
if (!mCalled) {
throw new RuntimeException(getClass().getSimpleName() + " did not call through to super.onEnterAnimationEnd(Animation)");
}
}
/**
* onCreateView
*/
protected abstract View onCreateView();
/**
* Corresponding to {@link #onCreateView()}, it called only when new UI (not cached UI)
* is created by {@link #onCreateView()}.
* It may be used to bind views to fragment and dynamically create child views such as
* {@link QMUITopBar#addLeftBackImageButton()}
*
* @param rootView the view created by {@link #onCreateView()}
*/
protected void onViewCreated(@NonNull View rootView) {
}
/**
* Will be performed in onStart
*
* @param requestCode request code
* @param resultCode result code
* @param data extra data
*
* @deprecated use {@link #registerEffect} for a replacement
*/
@Deprecated
protected void onFragmentResult(int requestCode, int resultCode, Intent data) {
}
/**
* disable or enable drag back
*
* @return if true open dragBack, otherwise close dragBack
* @deprecated Use {@link #getDragDirection(SwipeBackLayout, SwipeBackLayout.ViewMoveAction, float, float, float, float, float)}
*/
@Deprecated
protected boolean canDragBack() {
return true;
}
/**
* disable or enable drag back
* @param context context
* @param dragDirection gesture direction
* @param moveEdge view move edge
* @return if true open dragBack, otherwise close dragBack
*
* @deprecated Use {@link #getDragDirection(SwipeBackLayout, SwipeBackLayout.ViewMoveAction, float, float, float, float, float)}
*/
@Deprecated
protected boolean canDragBack(Context context, int dragDirection, int moveEdge) {
return canDragBack();
}
/**
* @return the init offset for backView for Parallax scrolling
* @deprecated Use {@link #backViewInitOffset(Context, int, int)}
*/
@Deprecated
protected int backViewInitOffset() {
return 0;
}
protected int backViewInitOffset(Context context, int dragDirection, int moveEdge) {
return backViewInitOffset();
}
/**
* called when drag back started.
*/
protected void onDragStart() {
}
/**
* @return
* @deprecated Use {@link #dragBackDirection()}
*/
@Deprecated
protected int dragBackEdge() {
return EDGE_LEFT;
}
/**
*
* @return
* @deprecated Use {@link #getDragDirection(SwipeBackLayout, SwipeBackLayout.ViewMoveAction, float, float, float, float, float)}
*/
@Deprecated
protected int dragBackDirection() {
int oldEdge = dragBackEdge();
if (oldEdge == EDGE_RIGHT) {
return SwipeBackLayout.DRAG_DIRECTION_RIGHT_TO_LEFT;
} else if (oldEdge == EDGE_TOP) {
return SwipeBackLayout.DRAG_DIRECTION_TOP_TO_BOTTOM;
} else if (oldEdge == EDGE_BOTTOM) {
return SwipeBackLayout.DRAG_DIRECTION_BOTTOM_TO_TOP;
}
return SwipeBackLayout.DRAG_DIRECTION_LEFT_TO_RIGHT;
}
protected SwipeBackLayout.ViewMoveAction dragViewMoveAction() {
return SwipeBackLayout.MOVE_VIEW_AUTO;
}
protected boolean canHandleSwipeBack(){
mCalled = true;
// 1. can not swipe back if enter animation is not finished
if (mEnterAnimationStatus != ANIMATION_ENTER_STATUS_END) {
return false;
}
Activity activity = getActivity();
if(activity == null || activity.isFinishing()){
return false;
}
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if (provider == null) {
return false;
}
FragmentManager fragmentManager = provider.getContainerFragmentManager();
// 3. is not managed by QMUIFragmentContainerProvider
if (fragmentManager == null || fragmentManager != getParentFragmentManager()) {
return false;
}
// 4. should handle by child
if(provider.isChildHandlePopBackRequested()){
return false;
}
// 5. can not swipe back if the view is null
View view = getView();
if (view == null) {
return false;
}
// 6. can not swipe back if the backStack entry count is less than 2
if ((fragmentManager.getBackStackEntryCount() <= 1 || mFinishActivityIfOnBackPressed) &&
!QMUISwipeBackActivityManager.getInstance().canSwipeBack(activity)) {
return false;
}
return true;
}
public void setFinishActivityIfOnBackPressed(boolean finishActivityIfOnBackPressed) {
mFinishActivityIfOnBackPressed = finishActivityIfOnBackPressed;
}
protected int getDragDirection(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull SwipeBackLayout.ViewMoveAction viewMoveAction,
float downX, float downY, float dx, float dy, float slopTouch) {
int targetDirection = dragBackDirection();
if (!canDragBack(swipeBackLayout.getContext(), targetDirection, viewMoveAction.getEdge(targetDirection))) {
return DRAG_DIRECTION_NONE;
}
int edgeSize = QMUIDisplayHelper.dp2px(swipeBackLayout.getContext(), 20);
if (targetDirection == DRAG_DIRECTION_LEFT_TO_RIGHT) {
if (downX < edgeSize && dx >= slopTouch) {
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_RIGHT_TO_LEFT) {
if (downX > swipeBackLayout.getWidth() - edgeSize && -dx >= slopTouch) {
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_TOP_TO_BOTTOM) {
if (downY < edgeSize && dy >= slopTouch) {
return targetDirection;
}
} else if (targetDirection == DRAG_DIRECTION_BOTTOM_TO_TOP) {
if (downY > swipeBackLayout.getHeight() - edgeSize && -dy >= slopTouch) {
return targetDirection;
}
}
return DRAG_DIRECTION_NONE;
}
/**
* the action will be performed before the start of the enter animation start or after the
* enter animation is finished
*
* @param runnable the action to perform
*/
public void runAfterAnimation(Runnable runnable) {
runAfterAnimation(runnable, false);
}
/**
* When data is rendered duration the transition animation, it will cause a choppy. this method
* will promise the data is rendered before or after transition animation
*
* @param runnable the action to perform
* @param onlyEnd if true, the action is only performed after the enter animation is finished,
* otherwise it can be performed before the start of the enter animation start
* or after the enter animation is finished.
*/
public void runAfterAnimation(Runnable runnable, boolean onlyEnd) {
Utils.assertInMainThread();
boolean ok = onlyEnd ? mEnterAnimationStatus == ANIMATION_ENTER_STATUS_END :
mEnterAnimationStatus != ANIMATION_ENTER_STATUS_STARTED;
if (ok) {
runnable.run();
} else {
if (mDelayRenderRunnableList == null) {
mDelayRenderRunnableList = new ArrayList<>(4);
}
mDelayRenderRunnableList.add(runnable);
}
}
/**
* some action, such as {@link #popBackStack()}, can not't invoked duration fragment-lifecycle,
* then we can call this method to ensure these actions is invoked after resumed.
* one use case is to call {@link #popBackStackAfterResume()} in {@link #onFragmentResult(int, int, Intent)}
*
* @param runnable
*/
public void runAfterResumed(Runnable runnable) {
Utils.assertInMainThread();
if (isResumed()) {
runnable.run();
} else {
if (mPostResumeRunnableList == null) {
mPostResumeRunnableList = new ArrayList<>(4);
}
mPostResumeRunnableList.add(runnable);
}
}
/**
* may not be call.
* @param animation
*/
protected void onEnterAnimationStart(@Nullable Animator animation) {
if (mCalled) {
throw new IllegalAccessError("don't call #onEnterAnimationStart() directly");
}
mCalled = true;
mEnterAnimationStatus = ANIMATION_ENTER_STATUS_STARTED;
isInEnterAnimationLiveData.setValue(true);
}
/**
* may not be call.
* @param animation
*/
protected void onEnterAnimationEnd(@Nullable Animator animation) {
if (mCalled) {
throw new IllegalAccessError("don't call #onEnterAnimationEnd() directly");
}
mCalled = true;
mEnterAnimationStatus = ANIMATION_ENTER_STATUS_END;
isInEnterAnimationLiveData.setValue(false);
notifyDelayRenderRunnableList();
}
private void notifyDelayRenderRunnableList(){
if (mDelayRenderRunnableList != null) {
ArrayList list = mDelayRenderRunnableList;
mDelayRenderRunnableList = null;
if (!list.isEmpty()) {
for (Runnable runnable : list) {
runnable.run();
}
}
}
}
public LiveData getIsInEnterAnimationLiveData() {
return isInEnterAnimationLiveData;
}
protected LiveData enterAnimationAvoidTransform(final LiveData origin){
return enterAnimationAvoidTransform(origin, isInEnterAnimationLiveData);
}
protected LiveData enterAnimationAvoidTransform(final LiveData origin, LiveData enterAnimationLiveData){
final MediatorLiveData result = new MediatorLiveData();
result.addSource(enterAnimationLiveData, new Observer(){
boolean isAdded = false;
@Override
public void onChanged(Boolean isInEnterAnimation) {
if(isInEnterAnimation){
isAdded = false;
result.removeSource(origin);
}else {
if(!isAdded){
isAdded = true;
result.addSource(origin, new Observer() {
@Override
public void onChanged(T t) {
result.setValue(t);
}
});
}
}
}
});
return result;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mSwipeBackgroundView != null) {
mSwipeBackgroundView.unBind();
mSwipeBackgroundView = null;
}
// help gc, sometimes user may hold fragment instance in somewhere,
// then these objects can not be released in time.
mCacheRootView = null;
mDelayRenderRunnableList = null;
mCheckPostResumeRunnable = null;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
return false;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
return false;
}
@WindowInsetsCompat.Type.InsetsType
public int getRootViewInsetsType() {
return getParentFragment() == null ? WindowInsetsCompat.Type.ime() : 0;
}
@Override
public void refreshFromScheme(@Nullable Bundle bundle) {
}
/**
* When finishing to pop back last fragment, let activity have a chance to do something
* like start a new fragment
*
* @return QMUIFragment to start a new fragment or Intent to start a new Activity
*/
@SuppressWarnings("SameReturnValue")
public Object onLastFragmentFinish() {
return null;
}
/**
* if intercepted, onLastFragmentFinish will not be invoked.
* @return
*/
protected boolean needInterceptLastFragmentFinish(){
Activity activity = getActivity();
return activity == null || !activity.isTaskRoot();
}
/**
* restore sub window(e.g dialog) when drag back to previous activity
*
* @return
*/
protected boolean restoreSubWindowWhenDragBack() {
return true;
}
public final boolean isStartedByScheme() {
Bundle arguments = getArguments();
return arguments != null && arguments.getBoolean(QMUISchemeHandler.ARG_FROM_SCHEME, false);
}
/**
* Fragment Transition Controller
*/
public TransitionConfig onFetchTransitionConfig() {
return SLIDE_TRANSITION_CONFIG;
}
public static final class TransitionConfig {
public final int enter;
public final int exit;
public final int popenter;
public final int popout;
public final int popenterAnimation;
public final int popoutAnimation;
public TransitionConfig(
int enter, int exit,
int popenter, int popout,
int popenterAnimation, int popoutAnimation
) {
this.enter = enter;
this.exit = exit;
this.popenter = popenter;
this.popout = popout;
// only use for pop activity if only one fragment exist.
this.popenterAnimation = popenterAnimation;
this.popoutAnimation = popoutAnimation;
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentActivity.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelStoreOwner;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.annotation.DefaultFirstFragment;
import com.qmuiteam.qmui.util.QMUIStatusBarHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* the container activity for {@link QMUIFragment}.
* Created by cgspine on 15/9/14.
*/
public abstract class QMUIFragmentActivity extends InnerBaseActivity implements QMUIFragmentContainerProvider {
public static final String QMUI_INTENT_DST_FRAGMENT_NAME = "qmui_intent_dst_fragment_name";
public static final String QMUI_INTENT_FRAGMENT_ARG = "qmui_intent_fragment_arg";
public static final String QMUI_INTENT_FRAGMENT_LIST_ARG = "qmui_intent_fragment_list_arg";
public static final String QMUI_MUTI_START_INDEX = "qmui_muti_start_index";
private static final String TAG = "QMUIFragmentActivity";
private RootView mRootView;
private FragmentAutoInitResult mFragmentAutoInitResult = FragmentAutoInitResult.unHandled;
private boolean isChildHandlePopBackRequested = false;
@Override
public int getContextViewId() {
return R.id.qmui_activity_fragment_container_id;
}
@Override
public FragmentManager getContainerFragmentManager() {
return getSupportFragmentManager();
}
public RootView getRootView() {
return mRootView;
}
@Override
@SuppressWarnings("unchecked")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
performTranslucent();
mRootView = onCreateRootView(getContextViewId());
setContentView(mRootView);
if (savedInstanceState == null) {
long start = System.currentTimeMillis();
Intent intent = getIntent();
// 1. handle muti fragments
mFragmentAutoInitResult = instantiationMutiFragment(intent);
if (mFragmentAutoInitResult == FragmentAutoInitResult.unHandled) {
try {
Class extends QMUIFragment> firstFragmentClass = null;
// 2. try get first fragment from fragment class name
String fragmentClassName = intent.getStringExtra(QMUI_INTENT_DST_FRAGMENT_NAME);
if (fragmentClassName != null && !fragmentClassName.isEmpty()) {
firstFragmentClass = (Class extends QMUIFragment>) Class.forName(fragmentClassName);
}
// 3. try get fragment from annotation @DefaultFirstFragment
if (firstFragmentClass == null) {
firstFragmentClass = getDefaultFirstFragment();
}
if (firstFragmentClass != null) {
QMUIFragment firstFragment = instantiationFragment(firstFragmentClass, intent.getBundleExtra(QMUI_INTENT_FRAGMENT_ARG));
if (firstFragment != null) {
getSupportFragmentManager()
.beginTransaction()
.add(getContextViewId(), firstFragment, firstFragment.getClass().getSimpleName())
.addToBackStack(firstFragment.getClass().getSimpleName())
.commit();
mFragmentAutoInitResult = FragmentAutoInitResult.success;
}
}
} catch (Exception e) {
QMUILog.d(TAG, "fragment auto inited: " + e.getMessage());
mFragmentAutoInitResult = FragmentAutoInitResult.failed;
}
}
Log.i(TAG, "the time it takes to inject first fragment from annotation is " + (System.currentTimeMillis() - start));
}
}
protected FragmentAutoInitResult instantiationMutiFragment(Intent intent) {
List fragmentBundles = intent.getParcelableArrayListExtra(QMUI_INTENT_FRAGMENT_LIST_ARG);
if (fragmentBundles != null && fragmentBundles.size() > 0) {
List fragments = new ArrayList<>(fragmentBundles.size());
for (Bundle bundle : fragmentBundles) {
String fragmentClassName = bundle.getString(QMUI_INTENT_DST_FRAGMENT_NAME);
try {
Class extends QMUIFragment> cls = (Class extends QMUIFragment>) Class.forName(fragmentClassName);
QMUIFragment fragment = instantiationFragment(cls, bundle.getBundle(QMUI_INTENT_FRAGMENT_ARG));
if (fragment == null) {
return FragmentAutoInitResult.failed;
}
fragments.add(fragment);
} catch (ClassNotFoundException e) {
QMUILog.d(TAG, "Can not find " + fragmentClassName);
}
}
if (fragments.size() > 0) {
initMutiFragment(fragments);
return FragmentAutoInitResult.success;
}
}
return FragmentAutoInitResult.unHandled;
}
protected boolean initMutiFragment(QMUIFragment... fragments) {
List list = new ArrayList<>(fragments.length);
Collections.addAll(list, fragments);
return initMutiFragment(list);
}
protected boolean initMutiFragment(List fragments) {
if (fragments.size() == 0) {
return false;
}
boolean disableSwipeBack = getIntent().getIntExtra(QMUIFragmentActivity.QMUI_MUTI_START_INDEX, 0) > 0;
if (fragments.size() == 1) {
QMUIFragment fragment = fragments.get(0);
if (disableSwipeBack) {
fragment.mDisableSwipeBackByMutiStarted = true;
}
String tagName = fragment.getClass().getSimpleName();
getSupportFragmentManager()
.beginTransaction()
.add(getContextViewId(), fragment, tagName)
.addToBackStack(tagName)
.commit();
return true;
}
ArrayList transactions = new ArrayList<>();
FragmentManager fragmentManager = getSupportFragmentManager();
for (int i = 0; i < fragments.size(); i++) {
QMUIFragment fragment = fragments.get(i);
if (disableSwipeBack) {
fragment.mDisableSwipeBackByMutiStarted = true;
}
disableSwipeBack = true;
FragmentTransaction transaction = fragmentManager.beginTransaction();
fragment.mDisableSwipeBackByMutiStarted = true;
String tagName = fragment.getClass().getSimpleName();
if (i == 0) {
transaction.add(getContextViewId(), fragment, tagName);
} else {
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
transaction.setCustomAnimations(0, 0, transitionConfig.popenter, transitionConfig.popout);
transaction.replace(getContextViewId(), fragment, tagName);
}
transaction.addToBackStack(tagName);
transaction.setReorderingAllowed(true);
transactions.add(transaction);
}
for (FragmentTransaction transaction : transactions) {
transaction.commit();
}
return true;
}
protected void performTranslucent() {
QMUIStatusBarHelper.translucent(this);
}
/**
* used for subclasses to see if the parent class initializes the first fragment。
* it must be called after super.onCreate in subclasses.
*
* @return true if first fragment is initialized.
*/
protected FragmentAutoInitResult isFragmentAutoInitResult() {
return mFragmentAutoInitResult;
}
protected void setFragmentAutoInitResult(FragmentAutoInitResult fragmentAutoInitResult) {
mFragmentAutoInitResult = fragmentAutoInitResult;
}
protected Class extends QMUIFragment> getDefaultFirstFragment() {
Class> cls = getClass();
while (cls != null && cls != QMUIFragmentActivity.class && QMUIFragmentActivity.class.isAssignableFrom(cls)) {
if (cls.isAnnotationPresent(DefaultFirstFragment.class)) {
DefaultFirstFragment defaultFirstFragment = cls.getAnnotation(DefaultFirstFragment.class);
if (defaultFirstFragment != null) {
return defaultFirstFragment.value();
}
}
cls = cls.getSuperclass();
}
return null;
}
protected QMUIFragment instantiationFragment(Class extends QMUIFragment> cls, Bundle args) {
try {
QMUIFragment fragment = cls.newInstance();
if (args != null) {
fragment.setArguments(args);
}
return fragment;
} catch (IllegalAccessException e) {
QMUILog.d(TAG, "Can not access " + cls.getName() + " for first fragment");
} catch (InstantiationException e) {
QMUILog.d(TAG, "Can not instance " + cls.getName() + " for first fragment");
}
return null;
}
@Override
public FragmentContainerView getFragmentContainerView() {
return mRootView.getFragmentContainerView();
}
@Override
public ViewModelStoreOwner getContainerViewModelStoreOwner() {
return this;
}
@Override
public void requestForHandlePopBack(boolean toHandle) {
isChildHandlePopBackRequested = toHandle;
}
@Override
public boolean isChildHandlePopBackRequested() {
return isChildHandlePopBackRequested;
}
protected RootView onCreateRootView(int fragmentContainerId) {
return new DefaultRootView(this, fragmentContainerId);
}
/**
* get the current Fragment.
*/
@Nullable
public Fragment getCurrentFragment() {
return getSupportFragmentManager().findFragmentById(getContextViewId());
}
@Nullable
private QMUIFragment getCurrentQMUIFragment() {
Fragment current = getCurrentFragment();
if (current instanceof QMUIFragment) {
return (QMUIFragment) current;
}
return null;
}
/**
* start a new fragment and then destroy current fragment.
* assume there is a fragment stack(A->B->C), and you use this method to start a new
* fragment D and destroy fragment C. Now you are in fragment D, if you want call
* {@link #popBackStack()} to back to B, what the animation should be? Sometimes we hope run
* animation generated by transition B->C, but sometimes we hope run animation generated by
* transition C->D. this why second parameter exists.
*
* @param fragment new fragment to start
* @param useNewTransitionConfigWhenPop if true, use animation generated by transition C->D,
* else, use animation generated by transition B->C
*/
public int startFragmentAndDestroyCurrent(QMUIFragment fragment, final boolean useNewTransitionConfigWhenPop) {
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.isDestroyed()) {
return -1;
}
if (fragmentManager.isStateSaved()) {
QMUILog.d(TAG, "startFragment can not be invoked after onSaveInstanceState");
return -1;
}
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
String tagName = fragment.getClass().getSimpleName();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
.setCustomAnimations(transitionConfig.enter, transitionConfig.exit,
transitionConfig.popenter, transitionConfig.popout)
.setPrimaryNavigationFragment(null)
.replace(getContextViewId(), fragment, tagName);
int index = transaction.commit();
Utils.modifyOpForStartFragmentAndDestroyCurrent(fragmentManager, fragment, useNewTransitionConfigWhenPop, transitionConfig);
return index;
}
/**
*
* @param fragment target fragment to start
* @return commit id
*
*/
public int startFragment(QMUIFragment fragment) {
Log.i(TAG, "startFragment");
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.isDestroyed()) {
return -1;
}
if (fragmentManager.isStateSaved()) {
QMUILog.d(TAG, "startFragment can not be invoked after onSaveInstanceState");
return -1;
}
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
String tagName = fragment.getClass().getSimpleName();
return fragmentManager.beginTransaction()
.setCustomAnimations(transitionConfig.enter, transitionConfig.exit, transitionConfig.popenter, transitionConfig.popout)
.replace(getContextViewId(), fragment, tagName)
.setPrimaryNavigationFragment(null)
.addToBackStack(tagName)
.commit();
}
public int startFragments(List fragments) {
Log.i(TAG, "startFragment");
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.isDestroyed()) {
return -1;
}
if (fragmentManager.isStateSaved()) {
QMUILog.d(TAG, "startFragment can not be invoked after onSaveInstanceState");
return -1;
}
if (fragments.size() == 0) {
return -1;
}
ArrayList transactions = new ArrayList<>();
QMUIFragment.TransitionConfig lastTransitionConfig = fragments.get(fragments.size() - 1).onFetchTransitionConfig();
for (QMUIFragment fragment : fragments) {
FragmentTransaction transaction = fragmentManager.beginTransaction().setPrimaryNavigationFragment(null);
QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
fragment.mDisableSwipeBackByMutiStarted = true;
String tagName = fragment.getClass().getSimpleName();
transaction.setCustomAnimations(transitionConfig.enter, lastTransitionConfig.exit, transitionConfig.popenter, transitionConfig.popout);
transaction.replace(getContextViewId(), fragment, tagName);
transaction.addToBackStack(tagName);
transactions.add(transaction);
transaction.setReorderingAllowed(true);
}
for (FragmentTransaction transaction : transactions) {
transaction.commit();
}
return 0;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
QMUIFragment fragment = getCurrentQMUIFragment();
if (fragment != null && !fragment.isInSwipeBack() && fragment.onKeyDown(keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
QMUIFragment fragment = getCurrentQMUIFragment();
if (fragment != null && !fragment.isInSwipeBack() && fragment.onKeyUp(keyCode, event)) {
return true;
}
return super.onKeyUp(keyCode, event);
}
public void popBackStack() {
getOnBackPressedDispatcher().onBackPressed();
}
public static Intent intentOf(@NonNull Context context,
@NonNull Class extends QMUIFragmentActivity> targetActivity,
@NonNull Class extends QMUIFragment> firstFragment) {
return intentOf(context, targetActivity, firstFragment, null);
}
/**
* create a intent for a new QMUIFragmentActivity
*
* @param context Generally it is activity
* @param targetActivity target activity class
* @param firstFragment first fragment in target activity
* @param fragmentArgs args for first fragment
* @return
*/
public static Intent intentOf(@NonNull Context context,
@NonNull Class extends QMUIFragmentActivity> targetActivity,
@NonNull Class extends QMUIFragment> firstFragment,
@Nullable Bundle fragmentArgs) {
Intent intent = new Intent(context, targetActivity);
intent.putExtra(QMUI_INTENT_DST_FRAGMENT_NAME, firstFragment.getName());
if (fragmentArgs != null) {
intent.putExtra(QMUI_INTENT_FRAGMENT_ARG, fragmentArgs);
}
return intent;
}
public static Intent intentOf(@NonNull Context context,
@NonNull Class extends QMUIFragmentActivity> targetActivity,
@NonNull String firstFragmentClassName,
@Nullable Bundle fragmentArgs) {
Intent intent = new Intent(context, targetActivity);
intent.putExtra(QMUI_INTENT_DST_FRAGMENT_NAME, firstFragmentClassName);
if (fragmentArgs != null) {
intent.putExtra(QMUI_INTENT_FRAGMENT_ARG, fragmentArgs);
}
return intent;
}
public static abstract class RootView extends FrameLayout {
public RootView(Context context, int fragmentContainerId) {
super(context);
setId(R.id.qmui_activity_root_id);
}
public abstract FragmentContainerView getFragmentContainerView();
}
@Override
public void onBackPressed() {
try {
super.onBackPressed();
} catch (Exception ignore) {
// 1. Under Android O, Activity#onBackPressed doesn't check FragmentManager's save state.
// 2. IndexOutOfBoundsException caused by ViewGroup#removeView(View) in EmotionUI.
}
}
@SuppressLint("ViewConstructor")
public static class DefaultRootView extends RootView {
private FragmentContainerView mFragmentContainerView;
public DefaultRootView(Context context, int fragmentContainerId) {
super(context, fragmentContainerId);
mFragmentContainerView = new FragmentContainerView(context);
mFragmentContainerView.setId(fragmentContainerId);
addView(mFragmentContainerView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override
public FragmentContainerView getFragmentContainerView() {
return mFragmentContainerView;
}
}
public enum FragmentAutoInitResult {success, failed, unHandled}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentContainerProvider.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelStoreOwner;
public interface QMUIFragmentContainerProvider {
int getContextViewId();
FragmentManager getContainerFragmentManager();
@Nullable
FragmentContainerView getFragmentContainerView();
ViewModelStoreOwner getContainerViewModelStoreOwner();
void requestForHandlePopBack(boolean toHandle);
boolean isChildHandlePopBackRequested();
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUIFragmentPagerAdapter.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.annotation.SuppressLint;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import com.qmuiteam.qmui.widget.QMUIPagerAdapter;
public abstract class QMUIFragmentPagerAdapter extends QMUIPagerAdapter {
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurrentTransaction;
private Fragment mCurrentPrimaryItem = null;
public QMUIFragmentPagerAdapter(@NonNull FragmentManager fm) {
mFragmentManager = fm;
}
public abstract QMUIFragment createFragment(int position);
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == ((Fragment) object).getView();
}
@SuppressLint("CommitTransaction")
@Override
@NonNull
protected Object hydrate(@NonNull ViewGroup container, int position) {
String name = makeFragmentName(container.getId(), position);
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
return fragment;
}
return createFragment(position);
}
@SuppressLint("CommitTransaction")
@Override
protected void populate(@NonNull ViewGroup container, @NonNull Object item, int position) {
String name = makeFragmentName(container.getId(), position);
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
mCurrentTransaction.attach(fragment);
if (fragment.getView() != null && fragment.getView().getWidth() == 0) {
fragment.getView().requestLayout();
}
} else {
fragment = (Fragment) item;
mCurrentTransaction.add(container.getId(), fragment, name);
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
mCurrentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
}
}
@SuppressLint("CommitTransaction")
@Override
protected void destroy(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
mCurrentTransaction.detach(fragment);
if (fragment == mCurrentPrimaryItem) {
mCurrentPrimaryItem = null;
}
}
@Override
public void startUpdate(@NonNull ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this
+ " requires a view id");
}
}
@Override
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitNowAllowingStateLoss();
mCurrentTransaction = null;
}
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
mCurrentTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
}
fragment.setMenuVisibility(true);
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
mCurrentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
mCurrentPrimaryItem = fragment;
}
}
private String makeFragmentName(int viewId, long id) {
return "QMUIFragmentPagerAdapter:" + viewId + ":" + id;
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUILatestVisit.java
================================================
package com.qmuiteam.qmui.arch;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.fragment.app.Fragment;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.record.DefaultLatestVisitStorage;
import com.qmuiteam.qmui.arch.record.QMUILatestVisitStorage;
import com.qmuiteam.qmui.arch.record.RecordArgumentEditor;
import com.qmuiteam.qmui.arch.record.RecordArgumentEditorImpl;
import com.qmuiteam.qmui.arch.record.RecordIdClassMap;
import java.util.Map;
public class QMUILatestVisit {
private static final String TAG = "QMUILatestVisit";
private static String NAV_STORE_PREFIX = "_qmui_nav";
private static String NAV_STORE_FRAGMENT_SUFFIX = ".class";
private static QMUILatestVisit sInstance;
private QMUILatestVisitStorage mStorage;
private Context mContext;
private RecordIdClassMap mRecordMap;
private RecordArgumentEditor mRecordArgumentEditor;
private RecordArgumentEditor mNavRecordArgumentEditor;
private QMUILatestVisit(Context context) {
mContext = context.getApplicationContext();
mRecordArgumentEditor = new RecordArgumentEditorImpl();
mNavRecordArgumentEditor = new RecordArgumentEditorImpl();
try {
Class> cls = Class.forName(RecordIdClassMap.class.getCanonicalName() + "Impl");
mRecordMap = (RecordIdClassMap) cls.newInstance();
} catch (ClassNotFoundException e) {
mRecordMap = new RecordIdClassMap() {
@Override
public Class> getRecordClassById(int id) {
return null;
}
@Override
public int getIdByRecordClass(Class> clazz) {
return QMUILatestVisitStorage.NOT_EXIST;
}
};
} catch (IllegalAccessException e) {
throw new RuntimeException("Can not access the Class RecordMetaMapImpl. " +
"Please file a issue to report this.");
} catch (InstantiationException e) {
throw new RuntimeException("Can not instance the Class RecordMetaMapImpl. " +
"Please file a issue to report this.");
}
}
@MainThread
public static QMUILatestVisit getInstance(Context context) {
if (sInstance == null) {
sInstance = new QMUILatestVisit(context);
}
return sInstance;
}
public static Intent intentOfLatestVisit(Activity activity) {
return getInstance(activity).getLatestVisitIntent(activity);
}
public void setStorage(QMUILatestVisitStorage storage) {
mStorage = storage;
}
QMUILatestVisitStorage getStorage() {
if (mStorage == null) {
mStorage = new DefaultLatestVisitStorage(mContext);
}
return mStorage;
}
@SuppressWarnings("unchecked")
private Intent getLatestVisitIntent(Context context) {
int activityId = getStorage().getActivityRecordId();
if (activityId == QMUILatestVisitStorage.NOT_EXIST) {
return null;
}
Class> activityCls = mRecordMap.getRecordClassById(activityId);
if (activityCls == null) {
return null;
}
Intent intent;
try {
if (QMUIFragmentActivity.class.isAssignableFrom(activityCls)) {
int fragmentId = getStorage().getFragmentRecordId();
if (fragmentId == QMUILatestVisitStorage.NOT_EXIST) {
return null;
}
Class> fragmentCls = mRecordMap.getRecordClassById(fragmentId);
if (fragmentCls == null) {
return null;
}
Class extends QMUIFragmentActivity> activity = (Class extends QMUIFragmentActivity>) activityCls;
Class extends QMUIFragment> fragment = (Class extends QMUIFragment>) fragmentCls;
Map arguments = getStorage().getFragmentArguments();
if (arguments == null || arguments.isEmpty()) {
intent = QMUIFragmentActivity.intentOf(context, activity, fragment, null);
} else {
Bundle bundle = new Bundle();
boolean hasNav = false;
for (String key : arguments.keySet()) {
if (key.startsWith(NAV_STORE_PREFIX)) {
hasNav = true;
} else {
RecordArgumentEditor.Argument argument = arguments.get(key);
if (argument != null) {
argument.putToBundle(bundle, key);
}
}
}
if (!hasNav) {
intent = QMUIFragmentActivity.intentOf(context, activity, fragment, bundle);
} else {
int navLevel = 0;
String fragmentClassName = fragment.getName();
while (true) {
String navPrefix = getNavFragmentStorePrefix(navLevel);
String navClassNameKey = navPrefix + NAV_STORE_FRAGMENT_SUFFIX;
RecordArgumentEditor.Argument navClassNameArg = arguments.get(navClassNameKey);
if (navClassNameArg == null) {
break;
}
bundle = QMUINavFragment.initArguments(fragmentClassName, bundle);
fragmentClassName = (String) navClassNameArg.getValue();
for (String key : arguments.keySet()) {
if (key.startsWith(navPrefix) && !key.equals(navClassNameKey)) {
RecordArgumentEditor.Argument arg = arguments.get(key);
if (arg != null) {
arg.putToBundle(bundle, key.substring(navPrefix.length()));
}
}
}
navLevel++;
}
intent = QMUIFragmentActivity.intentOf(context, activity, fragmentClassName, bundle);
}
}
} else {
intent = new Intent(context, activityCls);
}
getStorage().getAndWriteActivityArgumentsToIntent(intent);
return intent;
} catch (Throwable throwable) {
QMUILog.e(TAG, "getLatestVisitIntent failed.", throwable);
getStorage().clearAll();
}
return null;
}
void clearFragmentLatestVisitRecord() {
getStorage().clearFragmentStorage();
}
void clearActivityLatestVisitRecord() {
getStorage().clearActivityStorage();
}
void performLatestVisitRecord(QMUIFragment fragment) {
int id = mRecordMap.getIdByRecordClass(fragment.getClass());
if (id == QMUILatestVisitStorage.NOT_EXIST) {
return;
}
mRecordArgumentEditor.clear();
mNavRecordArgumentEditor.clear();
fragment.onCollectLatestVisitArgument(mRecordArgumentEditor);
Fragment parent = fragment.getParentFragment();
int level = 0;
while (parent instanceof QMUINavFragment) {
String navInfo = getNavFragmentStorePrefix(level);
QMUINavFragment nav = (QMUINavFragment) parent;
mNavRecordArgumentEditor.clear();
nav.onCollectLatestVisitArgument(mNavRecordArgumentEditor);
Map args = mNavRecordArgumentEditor.getAll();
mRecordArgumentEditor.putString(navInfo + NAV_STORE_FRAGMENT_SUFFIX, nav.getClass().getName());
for (String arg : args.keySet()) {
mRecordArgumentEditor.put(navInfo + arg, args.get(arg));
}
parent = parent.getParentFragment();
level++;
}
getStorage().saveFragmentRecordInfo(id, mRecordArgumentEditor.getAll());
mRecordArgumentEditor.clear();
mNavRecordArgumentEditor.clear();
}
void performLatestVisitRecord(InnerBaseActivity activity) {
int id = mRecordMap.getIdByRecordClass(activity.getClass());
if (id == QMUILatestVisitStorage.NOT_EXIST) {
return;
}
mRecordArgumentEditor.clear();
activity.onCollectLatestVisitArgument(mRecordArgumentEditor);
getStorage().saveActivityRecordInfo(id, mRecordArgumentEditor.getAll());
mRecordArgumentEditor.clear();
}
private String getNavFragmentStorePrefix(int level) {
return NAV_STORE_PREFIX + level + "_";
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUINavFragment.java
================================================
package com.qmuiteam.qmui.arch;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelStoreOwner;
import com.qmuiteam.qmui.QMUILog;
public class QMUINavFragment extends QMUIFragment implements QMUIFragmentContainerProvider {
private static final String TAG = "QMUINavFragment";
private static final String QMUI_ARGUMENT_DST_FRAGMENT = "qmui_argument_dst_fragment";
private static final String QMUI_ARGUMENT_FRAGMENT_ARG = "qmui_argument_fragment_arg";
private FragmentContainerView mContainerView;
private boolean mIsFirstFragmentAdded = false;
private boolean isChildHandlePopBackRequested = false;
public static QMUINavFragment getDefaultInstance(Class extends QMUIFragment> firstFragmentCls,
@Nullable Bundle firstFragmentArgument){
QMUINavFragment navFragment = new QMUINavFragment();
Bundle arg = new Bundle();
arg.putString(QMUI_ARGUMENT_DST_FRAGMENT, firstFragmentCls.getName());
arg.putBundle(QMUI_ARGUMENT_FRAGMENT_ARG, firstFragmentArgument);
navFragment.setArguments(initArguments(firstFragmentCls, firstFragmentArgument));
return navFragment;
}
public static Bundle initArguments(Class extends QMUIFragment> firstFragmentCls,
@Nullable Bundle firstFragmentArgument){
Bundle arg = new Bundle();
arg.putString(QMUI_ARGUMENT_DST_FRAGMENT, firstFragmentCls.getName());
arg.putBundle(QMUI_ARGUMENT_FRAGMENT_ARG, firstFragmentArgument);
return arg;
}
static Bundle initArguments(String firstFragmentClsName, @Nullable Bundle firstFragmentArgument){
Bundle arg = new Bundle();
arg.putString(QMUI_ARGUMENT_DST_FRAGMENT, firstFragmentClsName);
arg.putBundle(QMUI_ARGUMENT_FRAGMENT_ARG, firstFragmentArgument);
return arg;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onCreateFirstFragment();
}
}
public boolean isFirstFragmentAdded() {
return mIsFirstFragmentAdded;
}
protected void setFirstFragmentAdded(boolean firstFragmentAdded) {
mIsFirstFragmentAdded = firstFragmentAdded;
}
protected void onCreateFirstFragment(){
Bundle arguments = getArguments();
if (arguments != null) {
String dstFragmentName = arguments.getString(QMUI_ARGUMENT_DST_FRAGMENT);
QMUIFragment firstFragment = instantiationFirstFragment(dstFragmentName, arguments);
if (firstFragment != null) {
mIsFirstFragmentAdded = true;
getChildFragmentManager()
.beginTransaction()
.add(getContextViewId(), firstFragment, firstFragment.getClass().getSimpleName())
.addToBackStack(firstFragment.getClass().getSimpleName())
.commit();
}
}
}
@SuppressWarnings("unchecked")
private QMUIFragment instantiationFirstFragment(String clsName, Bundle arguments) {
try {
Class extends QMUIFragment> cls = (Class extends QMUIFragment>) Class.forName(clsName);
QMUIFragment fragment = cls.newInstance();
Bundle args = arguments.getBundle(QMUI_ARGUMENT_FRAGMENT_ARG);
if (args != null) {
fragment.setArguments(args);
}
return fragment;
} catch (IllegalAccessException e) {
QMUILog.d(TAG, "Can not access " + clsName + " for first fragment");
} catch (java.lang.InstantiationException e) {
QMUILog.d(TAG, "Can not instance " + clsName + " for first fragment");
} catch (ClassNotFoundException e) {
QMUILog.d(TAG, "Can not find " + clsName);
}
return null;
}
@Override
protected View onCreateView() {
FragmentContainerView rootView = new FragmentContainerView(getContext());
rootView.setId(getContextViewId());
return rootView;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mContainerView = view.findViewById(getContextViewId());
if(mContainerView == null){
throw new RuntimeException("must call #configFragmentContainerView() in onCreateView()");
}
}
protected void configFragmentContainerView(FragmentContainerView fragmentContainerView){
fragmentContainerView.setId(getContextViewId());
}
@Override
public void onDestroyView() {
super.onDestroyView();
mContainerView = null;
}
@Override
public int getContextViewId() {
return R.id.qmui_activity_fragment_container_id;
}
@Override
public void requestForHandlePopBack(boolean toHandle) {
isChildHandlePopBackRequested = toHandle;
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if(provider != null){
provider.requestForHandlePopBack(toHandle || getChildFragmentManager().getBackStackEntryCount() > 1);
}
}
@Override
public boolean isChildHandlePopBackRequested() {
return isChildHandlePopBackRequested;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getChildFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
checkForRequestForHandlePopBack();
if(getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)){
checkForPrimaryNavigation();
}
}
});
}
private void checkForPrimaryNavigation(){
getParentFragmentManager()
.beginTransaction()
.setPrimaryNavigationFragment(getChildFragmentManager().getBackStackEntryCount() > 1 ? QMUINavFragment.this : null)
.commit();
}
@Override
protected void checkForRequestForHandlePopBack(){
boolean enoughBackStackCount = getChildFragmentManager().getBackStackEntryCount() > 1;
QMUIFragmentContainerProvider provider = findFragmentContainerProvider(false);
if(provider != null){
provider.requestForHandlePopBack(isChildHandlePopBackRequested || enoughBackStackCount);
}
}
@Override
public void onResume() {
super.onResume();
checkForPrimaryNavigation();
}
@Override
public FragmentManager getContainerFragmentManager() {
return getChildFragmentManager();
}
@Override
public ViewModelStoreOwner getContainerViewModelStoreOwner() {
return this;
}
@Nullable
@Override
public FragmentContainerView getFragmentContainerView() {
return mContainerView;
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/QMUISwipeBackActivityManager.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Stack;
public class QMUISwipeBackActivityManager implements Application.ActivityLifecycleCallbacks {
private static QMUISwipeBackActivityManager sInstance;
private Stack mActivityStack = new Stack<>();
private Activity mCurrentActivity = null;
@MainThread
public static QMUISwipeBackActivityManager getInstance() {
if (sInstance == null) {
throw new IllegalAccessError("the QMUISwipeBackActivityManager is not initialized; " +
"please call QMUISwipeBackActivityManager.init(Application) in your application.");
}
return sInstance;
}
private QMUISwipeBackActivityManager() {
}
public static void init(@NonNull Application application) {
if (sInstance == null) {
sInstance = new QMUISwipeBackActivityManager();
application.registerActivityLifecycleCallbacks(sInstance);
}
}
@Override
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
if(mCurrentActivity == null){
mCurrentActivity = activity;
}
mActivityStack.add(activity);
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
mActivityStack.remove(activity);
if(mActivityStack.isEmpty()){
mCurrentActivity = null;
}
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
mCurrentActivity = activity;
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Nullable
public Activity getCurrentActivity(){
return mCurrentActivity;
}
public int getActivityCount(){
return mActivityStack.size();
}
@Nullable
public Activity getActivityInStack(int index){
if(index < 0 || index >= mActivityStack.size()){
return null;
}
return mActivityStack.get(index);
}
/**
*
* refer to https://github.com/bingoogolapple/BGASwipeBackLayout-Android/
* @param currentActivity the last activity
* @return
*/
@Nullable
public Activity getPenultimateActivity(Activity currentActivity) {
Activity activity = null;
try {
if (mActivityStack.size() > 1) {
activity = mActivityStack.get(mActivityStack.size() - 2);
if (currentActivity.equals(activity)) {
int index = mActivityStack.indexOf(currentActivity);
if (index > 0) {
// if memory leaks or the last activity is being finished
activity = mActivityStack.get(index - 1);
} else if (mActivityStack.size() == 2) {
// if screen orientation changes, there may be an error sequence in the stack
activity = mActivityStack.lastElement();
}
}
}
} catch (Exception ignored) {
}
return activity;
}
public boolean canSwipeBack(Activity currentActivity) {
if(currentActivity == null){
return false;
}
Activity prevActivity = getPenultimateActivity(currentActivity);
return prevActivity != null && !prevActivity.isDestroyed() && !prevActivity.isFinishing();
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackLayout.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.OverScroller;
import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.qmuiteam.qmui.util.QMUILangHelper;
import com.qmuiteam.qmui.util.QMUIViewOffsetHelper;
import com.qmuiteam.qmui.util.QMUIWindowInsetHelper;
import java.util.ArrayList;
import java.util.List;
import static com.qmuiteam.qmui.QMUIInterpolatorStaticHolder.QUNITIC_INTERPOLATOR;
/**
* Created by cgspine on 2018/1/7.
*
* modified from https://github.com/ikew0ng/SwipeBackLayout
*/
public class SwipeBackLayout extends FrameLayout {
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;
private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f;
private static final int BASE_SETTLE_DURATION = 256; // ms
private static final int MAX_SETTLE_DURATION = 600; // ms
public static final int DRAG_DIRECTION_NONE = 0;
public static final int DRAG_DIRECTION_LEFT_TO_RIGHT = 1;
public static final int DRAG_DIRECTION_RIGHT_TO_LEFT = 2;
public static final int DRAG_DIRECTION_TOP_TO_BOTTOM = 3;
public static final int DRAG_DIRECTION_BOTTOM_TO_TOP = 4;
public static final int EDGE_LEFT = 1;
public static final int EDGE_RIGHT = 2;
public static final int EDGE_TOP = 4;
public static final int EDGE_BOTTOM = 8;
public static final int STATE_IDLE = 0;
public static final int STATE_DRAGGING = 1;
public static final int STATE_SETTLING = 2;
public static final ViewMoveAction MOVE_VIEW_AUTO = new ViewMoveAuto();
public static final ViewMoveAction MOVE_VIEW_LEFT_TO_RIGHT = new ViewMoveLeftToRight();
public static final ViewMoveAction MOVE_VIEW_TOP_TO_BOTTOM = new ViewMoveTopToBottom();
private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD;
private View mContentView;
private List mListeners;
private Callback mCallback;
private OnInsetsHandler mOnInsetsHandler;
private Drawable mShadowLeft;
private Drawable mShadowRight;
private Drawable mShadowBottom;
private Drawable mShadowTop;
private float mScrimOpacity;
private int mScrimColor = DEFAULT_SCRIM_COLOR;
private VelocityTracker mVelocityTracker;
private float mMaxVelocity;
private float mMinVelocity;
private OverScroller mScroller;
private int mTouchSlop;
private float mInitialMotionX;
private float mInitialMotionY;
private float mLastMotionX;
private float mLastMotionY;
private int mDragState = STATE_IDLE;
private QMUIViewOffsetHelper mViewOffsetHelper;
private ViewMoveAction mViewMoveAction = MOVE_VIEW_AUTO;
private int mCurrentDragDirection = 0;
private boolean mIsScrollOverValid = true;
private boolean mEnableSwipeBack = true;
private int mRequestLayoutCount = 0;
private long mRequestLayoutCheckStartTime = -1;
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);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle,
R.style.SwipeBackLayout);
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);
int shadowTop = a.getResourceId(R.styleable.SwipeBackLayout_shadow_top,
R.drawable.shadow_top);
setShadow(shadowLeft, EDGE_LEFT);
setShadow(shadowRight, EDGE_RIGHT);
setShadow(shadowBottom, EDGE_BOTTOM);
setShadow(shadowTop, EDGE_TOP);
a.recycle();
final float density = getResources().getDisplayMetrics().density;
final float minVel = MIN_FLING_VELOCITY * density;
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = minVel;
mScroller = new OverScroller(context, QUNITIC_INTERPOLATOR);
QMUIWindowInsetHelper.setOnApplyWindowInsetsListener(this, new androidx.core.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
int insetsType = mOnInsetsHandler != null ? mOnInsetsHandler.getInsetsType() : 0;
if(insetsType != 0){
Insets toUsed = insets.getInsets(insetsType);
v.setPadding(toUsed.left, toUsed.top, toUsed.right, toUsed.bottom);
}else{
v.setPadding(0, 0, 0, 0);
}
return insets;
}
}, false);
}
public void setEnableSwipeBack(boolean enableSwipeBack) {
mEnableSwipeBack = enableSwipeBack;
}
public boolean isEnableSwipeBack() {
return mEnableSwipeBack;
}
private final Runnable mSetIdleRunnable = new Runnable() {
@Override
public void run() {
setDragState(STATE_IDLE);
}
};
/**
* Set up contentView which will be moved by user gesture
*
* @param view
*/
private void setContentView(View view) {
mContentView = view;
mViewOffsetHelper = new QMUIViewOffsetHelper(view);
}
public void setViewMoveAction(@NonNull ViewMoveAction viewMoveAction) {
mViewMoveAction = viewMoveAction;
}
public View getContentView() {
return mContentView;
}
public void setCallback(Callback callback) {
mCallback = callback;
}
/**
* 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();
}
/**
* 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 ListenerRemover addSwipeListener(final SwipeListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<>();
}
mListeners.add(listener);
return new ListenerRemover() {
@Override
public void remove() {
mListeners.remove(listener);
}
};
}
/**
* Removes a listener from the set of listeners
*
* @param listener
*/
public void removeSwipeListener(SwipeListener listener) {
if (mListeners == null) {
return;
}
mListeners.remove(listener);
}
public void clearSwipeListeners() {
if (mListeners == null) {
return;
}
mListeners.clear();
mListeners = null;
}
public void setOnInsetsHandler(OnInsetsHandler insetsHandler) {
mOnInsetsHandler = insetsHandler;
}
/**
* 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
* @param edgeFlag Combination of edge flags describing the edge to set
*/
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;
} else if ((edgeFlag & EDGE_TOP) != 0) {
mShadowTop = shadow;
}
invalidate();
}
/**
* Set a drawable used for edge shadow.
*
* @param resId Resource of drawable to use
* @param edgeFlag Combination of edge flags describing the edge to set
* @see #EDGE_LEFT
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void setShadow(int resId, int edgeFlag) {
setShadow(getResources().getDrawable(resId), edgeFlag);
}
void setDragState(int state) {
removeCallbacks(mSetIdleRunnable);
if (mDragState != state) {
mDragState = state;
onViewDragStateChanged(mDragState);
}
}
private boolean isTouchInContentView(float x, float y) {
return x >= mContentView.getLeft() && x < mContentView.getRight()
&& y >= mContentView.getTop() && y < mContentView.getBottom();
}
private int selectDragDirection(float x, float y) {
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
mCurrentDragDirection = mCallback == null ? DRAG_DIRECTION_NONE :
mCallback.getDragDirection(this, mViewMoveAction, mInitialMotionX, mInitialMotionY, dx, dy, mTouchSlop);
if(mCurrentDragDirection != DRAG_DIRECTION_NONE){
mInitialMotionX = mLastMotionX = x;
mInitialMotionY = mLastMotionY = y;
onSwipeBackBegin();
requestParentDisallowInterceptTouchEvent(true);
setDragState(STATE_DRAGGING);
}
return mCurrentDragDirection;
}
private float getTouchMoveDelta(float x, float y) {
if (mCurrentDragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT ||
mCurrentDragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT) {
return x - mLastMotionX;
} else {
return y - mLastMotionY;
}
}
@Override
public void requestLayout() {
super.requestLayout();
mRequestLayoutCount++;
if(mRequestLayoutCheckStartTime == -1){
mRequestLayoutCheckStartTime = SystemClock.elapsedRealtime();
}
if(mRequestLayoutCount >= 100){
long duration = SystemClock.elapsedRealtime() - mRequestLayoutCheckStartTime;
if(duration < 4000){
if(mCallback != null){
mCallback.reportFrequentlyRequestLayout(mRequestLayoutCount, duration);
}
}
mRequestLayoutCount = 0;
mRequestLayoutCheckStartTime = -1;
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mRequestLayoutCount=0;
mRequestLayoutCheckStartTime = -1;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(!mEnableSwipeBack){
cancel();
return false;
}
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = mLastMotionX = x;
mInitialMotionY = mLastMotionY = y;
if (mDragState == STATE_SETTLING) {
if (isTouchInContentView(x, y)) {
requestParentDisallowInterceptTouchEvent(true);
setDragState(STATE_DRAGGING);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_IDLE) {
selectDragDirection(x, y);
} else if (mDragState == STATE_DRAGGING) {
mViewMoveAction.move(this, mContentView, mViewOffsetHelper,
mCurrentDragDirection, getTouchMoveDelta(x, y));
onScroll();
} else {
if (isTouchInContentView(x, y)) {
requestParentDisallowInterceptTouchEvent(true);
setDragState(STATE_DRAGGING);
}
}
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
cancel();
break;
}
}
return mDragState == STATE_DRAGGING;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(!mEnableSwipeBack){
cancel();
return false;
}
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = mLastMotionX = x;
mInitialMotionY = mLastMotionY = y;
if (mDragState == STATE_SETTLING) {
if (isTouchInContentView(x, y)) {
requestParentDisallowInterceptTouchEvent(true);
setDragState(STATE_DRAGGING);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_IDLE) {
selectDragDirection(x, y);
} else if (mDragState == STATE_DRAGGING) {
mViewMoveAction.move(this, mContentView, mViewOffsetHelper,
mCurrentDragDirection, getTouchMoveDelta(x, y));
onScroll();
} else {
if (isTouchInContentView(x, y)) {
requestParentDisallowInterceptTouchEvent(true);
setDragState(STATE_DRAGGING);
}
}
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
settleContentViewAt(0, 0,
(int) mVelocityTracker.getXVelocity(),
(int) mVelocityTracker.getYVelocity());
}
cancel();
break;
}
}
return true;
}
private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
int moveEdge = mViewMoveAction.getEdge(mCurrentDragDirection);
float v;
if(mCurrentDragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT ||
mCurrentDragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT){
v = clampMag(mVelocityTracker.getXVelocity(), mMinVelocity, mMaxVelocity);
}else{
v = clampMag(mVelocityTracker.getYVelocity(), mMinVelocity, mMaxVelocity);
}
if (moveEdge == EDGE_LEFT || moveEdge == EDGE_RIGHT) {
int target = mViewMoveAction.getSettleTarget(this, mContentView,
v, mCurrentDragDirection, mScrollThreshold);
settleContentViewAt(target, 0, (int) v, 0);
} else {
int target = mViewMoveAction.getSettleTarget(this, mContentView,
v, mCurrentDragDirection, mScrollThreshold);
settleContentViewAt(0, target, 0, (int) v);
}
}
/**
* 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 settleContentViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mContentView.getLeft();
final int startTop = mContentView.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(dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
invalidate();
return true;
}
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
mViewOffsetHelper.setOffset(
x - mViewOffsetHelper.getLayoutLeft(),
y - mViewOffsetHelper.getLayoutTop());
onScroll();
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 = false;
}
if (!keepGoing) {
if (deferCallbacks) {
post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
private int computeSettleDuration(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 range = mViewMoveAction.getDragRange(this, mCurrentDragDirection);
int xduration = computeAxisDuration(dx, xvel, range);
int yduration = computeAxisDuration(dy, yvel, range);
return (int) (xduration * xweight + yduration * yweight);
}
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = 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 if (motionRange != 0) {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
} else {
duration = BASE_SETTLE_DURATION;
}
return Math.min(duration, MAX_SETTLE_DURATION);
}
private float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * (float) Math.PI / 2.0f;
return (float) Math.sin(f);
}
/**
* 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;
}
public void cancel() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mViewOffsetHelper != null) {
mViewOffsetHelper.onViewLayout();
}
}
@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
&& mDragState != 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);
int movingEdge = mViewMoveAction.getEdge(mCurrentDragDirection);
if ((movingEdge & EDGE_LEFT) != 0) {
canvas.clipRect(0, 0, child.getLeft(), getHeight());
} else if ((movingEdge & EDGE_RIGHT) != 0) {
canvas.clipRect(child.getRight(), 0, getRight(), getHeight());
} else if ((movingEdge & EDGE_BOTTOM) != 0) {
canvas.clipRect(0, child.getBottom(), getRight(), getHeight());
} else if ((movingEdge & EDGE_TOP) != 0) {
canvas.clipRect(0, 0, getRight(), child.getTop());
}
canvas.drawColor(color);
}
private void drawShadow(Canvas canvas, View child) {
int movingEdge = mViewMoveAction.getEdge(mCurrentDragDirection);
if ((movingEdge & EDGE_LEFT) != 0) {
mShadowLeft.setBounds(child.getLeft() - mShadowLeft.getIntrinsicWidth(),
child.getTop(), child.getLeft(), child.getBottom());
mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowLeft.draw(canvas);
} else if ((movingEdge & EDGE_RIGHT) != 0) {
mShadowRight.setBounds(child.getRight(), child.getTop(),
child.getRight() + mShadowRight.getIntrinsicWidth(), child.getBottom());
mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowRight.draw(canvas);
} else if ((movingEdge & EDGE_BOTTOM) != 0) {
mShadowBottom.setBounds(child.getLeft(), child.getBottom(), child.getRight(),
child.getBottom() + mShadowBottom.getIntrinsicHeight());
mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowBottom.draw(canvas);
} else if ((movingEdge & EDGE_TOP) != 0) {
mShadowTop.setBounds(child.getLeft(), child.getTop() - mShadowTop.getIntrinsicHeight(),
child.getRight(), child.getTop());
mShadowTop.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowTop.draw(canvas);
}
}
@Override
public void computeScroll() {
if (continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void onSwipeBackBegin() {
mIsScrollOverValid = true;
mScrimOpacity = 1 - mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection);
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onSwipeBackBegin(mCurrentDragDirection, mViewMoveAction.getEdge(mCurrentDragDirection));
}
}
invalidate();
}
private void onScroll() {
float scrollPercent = mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection);
mScrimOpacity = 1 - mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection);
if (scrollPercent < mScrollThreshold && !mIsScrollOverValid) {
mIsScrollOverValid = true;
}
if (mDragState == STATE_DRAGGING && mIsScrollOverValid &&
scrollPercent >= mScrollThreshold) {
mIsScrollOverValid = false;
onScrollOverThreshold();
}
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onScroll(mCurrentDragDirection, mViewMoveAction.getEdge(mCurrentDragDirection), scrollPercent);
}
}
invalidate();
}
private void onScrollOverThreshold() {
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onScrollOverThreshold();
}
}
}
private void onViewDragStateChanged(int dragState) {
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onScrollStateChange(dragState,
mViewMoveAction.getCurrentPercent(this, mContentView, mCurrentDragDirection));
}
}
}
public void resetOffset(){
if(mViewOffsetHelper != null){
mViewOffsetHelper.setOffset(0, 0);
}
}
public static SwipeBackLayout wrap(View child, ViewMoveAction viewMoveAction, Callback callback) {
SwipeBackLayout wrapper = new SwipeBackLayout(child.getContext());
wrapper.addView(child, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
wrapper.setContentView(child);
wrapper.setViewMoveAction(viewMoveAction);
wrapper.setCallback(callback);
return wrapper;
}
public static SwipeBackLayout wrap(Context context, int childRes, ViewMoveAction viewMoveAction, Callback callback) {
SwipeBackLayout wrapper = new SwipeBackLayout(context);
View child = LayoutInflater.from(context).inflate(childRes, wrapper, false);
wrapper.addView(child, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
wrapper.setContentView(child);
wrapper.setCallback(callback);
wrapper.setViewMoveAction(viewMoveAction);
return wrapper;
}
public static void translateInSwipeBack(View view, int edgeFlag, int targetOffset){
if (edgeFlag == EDGE_BOTTOM) {
view.setTranslationY(targetOffset);
view.setTranslationX(0);
} else if (edgeFlag == EDGE_RIGHT) {
view.setTranslationY(0);
view.setTranslationX(targetOffset);
} else if(edgeFlag == EDGE_LEFT){
view.setTranslationY(0);
view.setTranslationX(-targetOffset);
}else{
view.setTranslationY(-targetOffset);
view.setTranslationX(0);
}
}
public float getXFraction() {
int width = getWidth();
if(width == 0){
ViewParent parent = getParent();
if(parent instanceof ViewGroup){
width = ((ViewGroup)parent).getWidth();
}
}
return (width == 0) ? 0 : getX() / (float) width;
}
public void setXFraction(float xFraction) {
int width = getWidth();
if(width == 0){
ViewParent parent = getParent();
if(parent instanceof ViewGroup){
width = ((ViewGroup)parent).getWidth();
}
}
setX((width > 0) ? (xFraction * width) : 0);
}
public float getYFraction() {
int height = getHeight();
if(height == 0){
ViewParent parent = getParent();
if(parent instanceof ViewGroup){
height = ((ViewGroup)parent).getHeight();
}
}
return (height == 0) ? 0 : getY() / (float) height;
}
public void setYFraction(float yFraction) {
int height = getHeight();
if(height == 0){
ViewParent parent = getParent();
if(parent instanceof ViewGroup){
height = ((ViewGroup)parent).getHeight();
}
}
setY((height > 0) ? (yFraction * height) : 0);
}
public interface Callback {
int getDragDirection(SwipeBackLayout swipeBackLayout, ViewMoveAction moveAction,
float downX, float downY, float dx, float dy, float touchSlop);
void reportFrequentlyRequestLayout(int count, long duration);
}
public interface ViewMoveAction {
float getCurrentPercent(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView, int dragDirection);
int getDragRange(@NonNull SwipeBackLayout swipeBackLayout, int dragDirection);
int getSettleTarget(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
float v, int dragDirection, float scrollThreshold);
int getEdge(int dragDirection);
void move(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
@NonNull QMUIViewOffsetHelper offsetHelper,
int dragDirection, float delta);
}
public interface ListenerRemover {
void remove();
}
public 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
*/
void onScrollStateChange(int state, float scrollPercent);
/**
* Invoke when scrolling
*
* @param moveEdge flag to describe edge
* @param scrollPercent scroll percent of this view
*/
void onScroll(int dragDirection, int moveEdge, float scrollPercent);
/**
* Invoke when swipe back begin.
*/
void onSwipeBackBegin(int dragDirection, int moveEdge);
/**
* Invoke when scroll percent over the threshold for the first time
*/
void onScrollOverThreshold();
}
public interface OnInsetsHandler {
@WindowInsetsCompat.Type.InsetsType
int getInsetsType();
}
public static class ViewMoveAuto implements ViewMoveAction {
private boolean isHor(int dragDirection){
return dragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT ||
dragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT;
}
@Override
public float getCurrentPercent(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView, int dragDirection) {
float percent;
if(isHor(dragDirection)){
percent = Math.abs(contentView.getLeft() * 1f / swipeBackLayout.getWidth());
}else{
percent = Math.abs(contentView.getTop() * 1f / swipeBackLayout.getHeight());
}
return QMUILangHelper.constrain(percent, 0f, 1f);
}
@Override
public int getDragRange(@NonNull SwipeBackLayout swipeBackLayout, int dragDirection) {
if(isHor(dragDirection)){
return swipeBackLayout.getWidth();
}
return swipeBackLayout.getHeight();
}
@Override
public int getSettleTarget(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
float v, int dragDirection, float scrollThreshold) {
if(dragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT){
if (v > 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return swipeBackLayout.getWidth();
}
}else if(dragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT){
if (v < 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return -swipeBackLayout.getWidth();
}
}else if(dragDirection == DRAG_DIRECTION_TOP_TO_BOTTOM){
if (v > 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return swipeBackLayout.getHeight();
}
}else{
if (v < 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return -swipeBackLayout.getHeight();
}
}
return 0;
}
@Override
public int getEdge(int dragDirection) {
if(dragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT){
return EDGE_LEFT;
}else if(dragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT){
return EDGE_RIGHT;
}else if(dragDirection == DRAG_DIRECTION_TOP_TO_BOTTOM){
return EDGE_TOP;
}else{
return EDGE_BOTTOM;
}
}
@Override
public void move(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
@NonNull QMUIViewOffsetHelper offsetHelper,
int dragDirection, float delta) {
if(dragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT){
int target = (int) (offsetHelper.getLeftAndRightOffset() + delta);
target = QMUILangHelper.constrain(target, 0, swipeBackLayout.getWidth());
offsetHelper.setLeftAndRightOffset(target);
}else if(dragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT){
int target = (int) (offsetHelper.getLeftAndRightOffset() + delta);
target = QMUILangHelper.constrain(target, -swipeBackLayout.getWidth(),0);
offsetHelper.setLeftAndRightOffset(target);
}else if(dragDirection == DRAG_DIRECTION_TOP_TO_BOTTOM){
int target = (int) (offsetHelper.getTopAndBottomOffset() + delta);
target = QMUILangHelper.constrain(target, 0, swipeBackLayout.getHeight());
offsetHelper.setTopAndBottomOffset(target);
}else{
int target = (int) (offsetHelper.getTopAndBottomOffset() + delta);
target = QMUILangHelper.constrain(target, -swipeBackLayout.getHeight(),0);
offsetHelper.setTopAndBottomOffset(target);
}
}
}
public static class ViewMoveLeftToRight implements ViewMoveAction {
@Override
public float getCurrentPercent(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView, int dragDirection) {
return QMUILangHelper.constrain(
contentView.getLeft() * 1f / swipeBackLayout.getWidth(), 0f, 1f);
}
@Override
public int getDragRange(@NonNull SwipeBackLayout swipeBackLayout, int dragDirection) {
return swipeBackLayout.getWidth();
}
@Override
public int getSettleTarget(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
float v, int dragDirection, float scrollThreshold) {
if (v > 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return swipeBackLayout.getWidth();
}
return 0;
}
@Override
public int getEdge(int dragDirection) {
return EDGE_LEFT;
}
@Override
public void move(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
@NonNull QMUIViewOffsetHelper offsetHelper, int dragDirection, float delta) {
if (dragDirection == DRAG_DIRECTION_BOTTOM_TO_TOP ||
dragDirection == DRAG_DIRECTION_TOP_TO_BOTTOM) {
delta = delta * swipeBackLayout.getWidth() / swipeBackLayout.getHeight();
}
int target = (int) (offsetHelper.getLeftAndRightOffset() + delta);
target = QMUILangHelper.constrain(target, 0, swipeBackLayout.getWidth());
offsetHelper.setLeftAndRightOffset(target);
}
}
public static class ViewMoveTopToBottom implements ViewMoveAction {
@Override
public float getCurrentPercent(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView, int dragDirection) {
return QMUILangHelper.constrain(
contentView.getTop() * 1f / swipeBackLayout.getHeight(), 0f, 1f);
}
@Override
public int getDragRange(@NonNull SwipeBackLayout swipeBackLayout, int dragDirection) {
return swipeBackLayout.getHeight();
}
@Override
public int getSettleTarget(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
float v, int dragDirection, float scrollThreshold) {
if (v > 0 ||
(v == 0 && getCurrentPercent(swipeBackLayout, contentView, dragDirection) > scrollThreshold)) {
return swipeBackLayout.getHeight();
}
return 0;
}
@Override
public int getEdge(int dragDirection) {
return EDGE_TOP;
}
@Override
public void move(@NonNull SwipeBackLayout swipeBackLayout,
@NonNull View contentView,
@NonNull QMUIViewOffsetHelper offsetHelper, int dragDirection, float delta) {
if (dragDirection == DRAG_DIRECTION_LEFT_TO_RIGHT ||
dragDirection == DRAG_DIRECTION_RIGHT_TO_LEFT) {
delta = delta * swipeBackLayout.getHeight() / swipeBackLayout.getWidth();
}
int target = (int) (offsetHelper.getTopAndBottomOffset() + delta);
target = QMUILangHelper.constrain(target, 0, swipeBackLayout.getHeight());
offsetHelper.setTopAndBottomOffset(target);
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/SwipeBackgroundView.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.qmuiteam.qmui.util.QMUIColorHelper;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class SwipeBackgroundView extends View {
private ArrayList mViewWeakReference;
private boolean mDoRotate = false;
public SwipeBackgroundView(Context context, boolean forceDisableHardwareAccelerated) {
super(context);
if(forceDisableHardwareAccelerated){
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
}
public void bind(Activity activity, Activity swipeActivity, boolean restoreForSubWindow) {
mDoRotate = false;
if (mViewWeakReference != null) {
mViewWeakReference.clear();
}
int orientation = activity.getResources().getConfiguration().orientation;
if (orientation != getResources().getConfiguration().orientation) {
// the screen orientation changed, reMeasure and reLayout
int requestedOrientation = activity.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE ||
requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
// TODO is it suitable for fixed screen orientation
// the prev activity has locked the screen orientation
mDoRotate = true;
} else if (swipeActivity instanceof InnerBaseActivity) {
swipeActivity.getWindow().getDecorView().setBackgroundColor(0);
((InnerBaseActivity) swipeActivity).convertToTranslucentCauseOrientationChanged();
invalidate();
return;
}
}
if (!restoreForSubWindow) {
View contentView = activity.findViewById(Window.ID_ANDROID_CONTENT);
if (mViewWeakReference == null) {
mViewWeakReference = new ArrayList<>();
}
mViewWeakReference.add(new ViewInfo(contentView, null, true));
invalidate();
return;
}
try {
IBinder windowToken = activity.getWindow().getDecorView().getWindowToken();
Field windowManagerGlobalField = activity.getWindowManager().getClass().getDeclaredField("mGlobal");
windowManagerGlobalField.setAccessible(true);
Object windowManagerGlobal = windowManagerGlobalField.get(activity.getWindowManager());
if (windowManagerGlobal != null) {
Field viewsField = windowManagerGlobal.getClass().getDeclaredField("mViews");
viewsField.setAccessible(true);
Field paramsField = windowManagerGlobal.getClass().getDeclaredField("mParams");
paramsField.setAccessible(true);
List params = (List) paramsField.get(windowManagerGlobal);
List views = (List) viewsField.get(windowManagerGlobal);
IBinder activityToken = null;
// reverse order
for (int i = params.size() - 1; i >= 0; i--) {
WindowManager.LayoutParams lp = params.get(i);
View view = views.get(i);
if (view.getWindowToken() == windowToken) {
activityToken = lp.token;
break;
}
}
if (activityToken != null) {
// reverse order
for (int i = params.size() - 1; i >= 0; i--) {
WindowManager.LayoutParams lp = params.get(i);
View view = views.get(i);
boolean isMain = view.getWindowToken() == windowToken;
// Dialog use activityToken in lp
// PopupWindow use windowToken in lp
if (isMain || lp.token == activityToken || lp.token == windowToken) {
View prevContentView = view.findViewById(Window.ID_ANDROID_CONTENT);
if (mViewWeakReference == null) {
mViewWeakReference = new ArrayList<>();
}
if (prevContentView != null) {
mViewWeakReference.add(new ViewInfo(prevContentView, lp, isMain));
}else {
// PopupWindow doest not exist a descendant view with id Window.ID_ANDROID_CONTENT
mViewWeakReference.add(new ViewInfo(view, lp, isMain));
}
}
}
}
}
} catch (Exception ignored) {
} finally {
// sure get one view
if (mViewWeakReference == null || mViewWeakReference.isEmpty()) {
View contentView = activity.findViewById(Window.ID_ANDROID_CONTENT);
if (mViewWeakReference == null) {
mViewWeakReference = new ArrayList<>();
}
mViewWeakReference.add(new ViewInfo(contentView, null, true));
}
}
invalidate();
}
public void unBind() {
if (mViewWeakReference != null) {
mViewWeakReference.clear();
}
mViewWeakReference = null;
mDoRotate = false;
}
boolean hasChildWindow() {
return mViewWeakReference != null && mViewWeakReference.size() > 1;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mViewWeakReference != null && mViewWeakReference.size() > 0) {
if (mDoRotate) {
canvas.translate(0, getHeight());
canvas.rotate(-90, 0, 0);
}
// reverse order
for (int i = mViewWeakReference.size() - 1; i >= 0; i--) {
mViewWeakReference.get(i).draw(canvas);
}
}
}
static class ViewInfo {
WeakReference viewRef;
WindowManager.LayoutParams lp;
boolean isMain;
private int[] tempLocations = new int[2];
public ViewInfo(@NonNull View view, @Nullable WindowManager.LayoutParams lp, boolean isMain) {
this.viewRef = new WeakReference<>(view);
this.lp = lp;
this.isMain = isMain;
}
void draw(Canvas canvas) {
View view = viewRef.get();
if (view != null) {
if (isMain || lp == null) {
view.draw(canvas);
} else {
if((lp.flags & WindowManager.LayoutParams.FLAG_DIM_BEHIND) != 0){
canvas.drawColor(QMUIColorHelper.setColorAlpha(Color.BLACK, lp.dimAmount));
}
view.getLocationOnScreen(tempLocations);
canvas.translate(tempLocations[0], tempLocations[1]);
view.draw(canvas);
canvas.translate(-tempLocations[0], -tempLocations[1]);
}
}
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityOptions;
import android.os.Looper;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.qmuiteam.qmui.QMUILog;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
/**
* Created by Chaojun Wang on 6/9/14.
*/
public class Utils {
private Utils() {
}
/**
* 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 {
@SuppressLint("PrivateApi") Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
method.setAccessible(true);
method.invoke(activity);
} catch (Throwable ignore) {
}
}
/**
* Convert a translucent themed Activity
* {@link android.R.attr#windowIsTranslucent} back from opaque to
* translucent following a call to
* {@link #convertActivityFromTranslucent(android.app.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) {
try {
@SuppressLint({"PrivateApi", "DiscouragedPrivateApi"}) 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;
}
}
@SuppressLint("PrivateApi") Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
translucentConversionListenerClazz, ActivityOptions.class);
convertToTranslucent.setAccessible(true);
convertToTranslucent.invoke(activity, null, options);
} catch (Throwable ignore) {
}
}
public static void assertInMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
String methodMsg = null;
if (elements != null && elements.length >= 4) {
methodMsg = elements[3].toString();
}
throw new IllegalStateException("Call the method must be in main thread: " + methodMsg);
}
}
static void modifyOpForStartFragmentAndDestroyCurrent(FragmentManager fragmentManager,
final QMUIFragment fragment,
final boolean useNewTransitionConfigWhenPop,
final QMUIFragment.TransitionConfig transitionConfig){
findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() {
@Override
public boolean handle(Object op) {
Field cmdField = null;
try {
cmdField = Utils.getOpCmdField(op);
cmdField.setAccessible(true);
int cmd = (int) cmdField.get(op);
if (cmd == 1) {
if (useNewTransitionConfigWhenPop) {
Field popEnterAnimField = Utils.getOpPopEnterAnimField(op);
popEnterAnimField.setAccessible(true);
popEnterAnimField.set(op, transitionConfig.popenter);
Field popExitAnimField = Utils.getOpPopExitAnimField(op);
popExitAnimField.setAccessible(true);
popExitAnimField.set(op, transitionConfig.popout);
}
Field oldFragmentField = Utils.getOpFragmentField(op);
oldFragmentField.setAccessible(true);
Object fragmentObj = oldFragmentField.get(op);
oldFragmentField.set(op, fragment);
Field backStackNestField = Fragment.class.getDeclaredField("mBackStackNesting");
backStackNestField.setAccessible(true);
int oldFragmentBackStackNest = (int) backStackNestField.get(fragmentObj);
backStackNestField.set(fragment, oldFragmentBackStackNest);
backStackNestField.set(fragmentObj, --oldFragmentBackStackNest);
return true;
}
} catch (Throwable e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean needReNameTag() {
return true;
}
@Override
public String newTagName() {
return fragment.getClass().getSimpleName();
}
});
}
static void findAndModifyOpInBackStackRecord(FragmentManager fragmentManager, int backStackIndex, OpHandler handler) {
if (fragmentManager == null || handler == null) {
return;
}
int backStackCount = fragmentManager.getBackStackEntryCount();
if (backStackCount > 0) {
if (backStackIndex >= backStackCount || backStackIndex < -backStackCount) {
QMUILog.d("findAndModifyOpInBackStackRecord", "backStackIndex error: " +
"backStackIndex = " + backStackIndex + " ; backStackCount = " + backStackCount);
return;
}
if (backStackIndex < 0) {
backStackIndex = backStackCount + backStackIndex;
}
try {
FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(backStackIndex);
if (handler.needReNameTag()) {
Field nameField = Utils.getNameField(backStackEntry);
if (nameField != null) {
nameField.setAccessible(true);
nameField.set(backStackEntry, handler.newTagName());
}
}
Field opsField = Utils.getOpsField(backStackEntry);
if(opsField != null){
opsField.setAccessible(true);
Object opsObj = opsField.get(backStackEntry);
if (opsObj instanceof List>) {
List> ops = (List>) opsObj;
for (Object op : ops) {
if (handler.handle(op)) {
return;
}
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
private static boolean sOldBackStackEntryImpl = false;
static Field getBackStackEntryField(FragmentManager.BackStackEntry backStackEntry, String name) {
Field opsField = null;
if (!sOldBackStackEntryImpl) {
try {
opsField = FragmentTransaction.class.getDeclaredField(name);
} catch (NoSuchFieldException ignore) {
}
}
if (opsField == null) {
sOldBackStackEntryImpl = true;
try {
opsField = backStackEntry.getClass().getDeclaredField(name);
} catch (NoSuchFieldException ignore) {
}
}
return opsField;
}
static Field getOpsField(FragmentManager.BackStackEntry backStackEntry) {
return getBackStackEntryField(backStackEntry, "mOps");
}
static Field getNameField(FragmentManager.BackStackEntry backStackEntry) {
return getBackStackEntryField(backStackEntry, "mName");
}
private static boolean sOldOpImpl = false;
private static Field getOpField(Object op, String fieldNameNew, String fieldNameOld) {
Field field = null;
if (!sOldOpImpl) {
try {
field = op.getClass().getDeclaredField(fieldNameNew);
} catch (NoSuchFieldException ignore) {
}
}
if (field == null) {
sOldOpImpl = true;
try {
field = op.getClass().getDeclaredField(fieldNameOld);
} catch (NoSuchFieldException ignore) {
}
}
return field;
}
static Field getOpCmdField(Object op) {
return getOpField(op, "mCmd", "cmd");
}
static Field getOpFragmentField(Object op) {
return getOpField(op, "mFragment", "fragment");
}
static Field getOpPopEnterAnimField(Object op) {
return getOpField(op, "mPopEnterAnim", "popEnterAnim");
}
static Field getOpPopExitAnimField(Object op) {
return getOpField(op, "mPopExitAnim", "popExitAnim");
}
interface OpHandler {
boolean handle(Object op);
boolean needReNameTag();
String newTagName();
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/annotation/DefaultFirstFragment.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.annotation;
import com.qmuiteam.qmui.arch.QMUIFragment;
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.TYPE)
public @interface DefaultFirstFragment {
Class extends QMUIFragment> value();
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/Effect.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
public abstract class Effect {
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/FragmentResultEffect.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
import android.content.Intent;
import androidx.annotation.Nullable;
public class FragmentResultEffect extends Effect {
private final int mRequestFragmentUUid;
private final int mResultCode;
private final int mRequestCode;
@Nullable
private final Intent mIntent;
public FragmentResultEffect(int requestFragmentUUid, int resultCode, int requestCode, @Nullable Intent intent) {
mRequestFragmentUUid = requestFragmentUUid;
mResultCode = resultCode;
mRequestCode = requestCode;
mIntent = intent;
}
public int getRequestCode() {
return mRequestCode;
}
public int getResultCode() {
return mResultCode;
}
public Intent getIntent() {
return mIntent;
}
public int getRequestFragmentUUid() {
return mRequestFragmentUUid;
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/MapEffect.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
import java.util.Map;
public class MapEffect extends Effect {
private final Map mMap;
public MapEffect(Map map) {
mMap = map;
}
public Object getValue(String key) {
if (mMap == null) {
return null;
}
return mMap.get(key);
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectHandler.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import java.util.List;
public abstract class QMUIFragmentEffectHandler {
public enum HandlePolicy {
/**
* handle the effect immediately without lifeCycle check
*/
Immediately,
/**
* handle the effect immediately if the lifecycle is after started.
*/
ImmediatelyIfStarted,
/**
* handle the effect util next start event.
*/
NextStartEvent
}
/**
* provide the handle policy to determine when to handle the effects.
* @return handle policy
*/
public HandlePolicy provideHandlePolicy() {
return HandlePolicy.ImmediatelyIfStarted;
}
/**
* determine whether we need handle the effect or not.
* @param effect the effect to check
* @return true if we need handle the effect
*/
public abstract boolean shouldHandleEffect(@NonNull T effect);
/**
* the time to handle effect depends on {@link HandlePolicy}.
* @param effect
*/
public abstract void handleEffect(@NonNull T effect);
/**
* if the handle policy is not {@link HandlePolicy#Immediately}, we may need handle more than one effects.
* @param effects
*/
public void handleEffect(@NonNull List effects) {
for (T effect : effects) {
handleEffect(effect);
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistration.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
public interface QMUIFragmentEffectRegistration {
void unregister();
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentEffectRegistry.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
import android.util.ArraySet;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModel;
import com.qmuiteam.qmui.QMUIConfig;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.QMUIFragment;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class QMUIFragmentEffectRegistry extends ViewModel {
class PendingRegister implements QMUIFragmentEffectRegistration{
final LifecycleOwner lifecycleOwner;
final QMUIFragmentEffectHandler effectHandler;
private QMUIFragmentEffectRegistration registration;
public PendingRegister(LifecycleOwner lifecycleOwner, QMUIFragmentEffectHandler effectHandler){
this.lifecycleOwner = lifecycleOwner;
this.effectHandler = effectHandler;
}
public void doRegister(){
if(lifecycleOwner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.DESTROYED)){
return;
}
registration = register(lifecycleOwner, effectHandler);
}
@Override
public void unregister() {
if(registration != null){
registration.unregister();
}
}
}
private static final String TAG = "FragmentEffectRegistry";
private final AtomicInteger mNextRc = new AtomicInteger(0);
private final transient Map> mKeyToHandler = new HashMap<>();
private transient int mNotifyEffectRunning = 0;
private final transient Set mPendingRemoveKeys = new HashSet<>();
private final transient List> mPendingRegister = new ArrayList<>();
/**
* Register a new handler with this registry.
*
* This is normally called by a higher level convenience methods like
* {@link QMUIFragment#registerEffect}.
*
* @param lifecycleOwner a {@link LifecycleOwner} that makes this call.
* @param effectHandler the handler to handle effect
*
* @return a FragmentEffectRegistration that can be used to unregister an FragmentEffectHandler.
*/
public QMUIFragmentEffectRegistration register(
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final QMUIFragmentEffectHandler effectHandler) {
if(mNotifyEffectRunning > 0){
PendingRegister pendingRegister = new PendingRegister<>(lifecycleOwner, effectHandler);
mPendingRegister.add(pendingRegister);
return pendingRegister;
}
final int rc = mNextRc.getAndIncrement();
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
mKeyToHandler.put(rc, new EffectHandlerWrapper(effectHandler, lifecycle));
lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner1, event) -> {
if (Lifecycle.Event.ON_DESTROY.equals(event)) {
unregister(rc);
}
});
return () -> QMUIFragmentEffectRegistry.this.unregister(rc);
}
/**
* Unregister a handler previously registered with {@link #register}. This shouldn't be
* called directly, but instead through {@link QMUIFragmentEffectRegistration#unregister()}.
*
* @param key the unique key used when registering a callback.
*/
@MainThread
final void unregister(int key) {
if(mNotifyEffectRunning > 0){
mPendingRemoveKeys.add(key);
return;
}
safeUnregister(key);
}
private void safeUnregister(int key){
EffectHandlerWrapper> effectHandlerWrapper = mKeyToHandler.remove(key);
if (effectHandlerWrapper != null) {
effectHandlerWrapper.cancel();
}
}
/**
* notify the effect to handlers registered with {@link #register}.
*
* This is normally called by a higher level convenience methods like
* {@link QMUIFragment#notifyEffect}
* @param effect
*/
public void notifyEffect(T effect) {
mNotifyEffectRunning++;
for (Integer key : mKeyToHandler.keySet()) {
EffectHandlerWrapper> wrapper = mKeyToHandler.get(key);
if (wrapper != null && wrapper.shouldHandleEffect(effect)) {
wrapper.pushOrHandleEffect(effect);
}
}
mNotifyEffectRunning--;
if(mNotifyEffectRunning == 0){
if(!mPendingRemoveKeys.isEmpty()){
for(Integer key: mPendingRemoveKeys){
safeUnregister(key);
}
mPendingRemoveKeys.clear();
}
if(!mPendingRegister.isEmpty()){
for(PendingRegister> register: mPendingRegister){
register.doRegister();
}
mPendingRegister.clear();
}
}
}
private static class EffectHandlerWrapper implements LifecycleEventObserver {
final QMUIFragmentEffectHandler mHandler;
final Lifecycle mLifecycle;
ArrayList mEffects = null;
final Class extends Effect> mEffectType;
EffectHandlerWrapper(QMUIFragmentEffectHandler handler, Lifecycle lifecycle) {
mHandler = handler;
mLifecycle = lifecycle;
lifecycle.addObserver(this);
mEffectType = getHandlerEffectType(handler);
}
@SuppressWarnings("unchecked")
private Class extends Effect> getHandlerEffectType(QMUIFragmentEffectHandler handler) {
Class extends Effect> effectClz = null;
try {
Class> handlerCls = handler.getClass();
while (handlerCls != null && handlerCls.getSuperclass() != QMUIFragmentEffectHandler.class) {
handlerCls = handlerCls.getSuperclass();
}
if (handlerCls != null) {
Type type = handlerCls.getGenericSuperclass();
if (type instanceof ParameterizedType) {
Type[] params = ((ParameterizedType) type).getActualTypeArguments();
if (params.length > 0) {
effectClz = (Class extends Effect>) params[0];
}
}
}
} catch (Throwable ignore) {
}
if (effectClz == null) {
if (QMUIConfig.DEBUG) {
throw new RuntimeException("Error to get FragmentEffectHandler's generic parameter type");
} else {
QMUILog.d(TAG, "Error to get FragmentEffectHandler's generic parameter type");
}
}
return effectClz;
}
@SuppressWarnings("unchecked")
boolean shouldHandleEffect(Effect effect) {
return mEffectType != null && mEffectType.isAssignableFrom(effect.getClass()) && mHandler.shouldHandleEffect((T) effect);
}
@MainThread
@SuppressWarnings("unchecked")
void pushOrHandleEffect(Effect effect) {
QMUIFragmentEffectHandler.HandlePolicy policy = mHandler.provideHandlePolicy();
if (policy == QMUIFragmentEffectHandler.HandlePolicy.Immediately ||
(policy == QMUIFragmentEffectHandler.HandlePolicy.ImmediatelyIfStarted &&
mLifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED))) {
mHandler.handleEffect((T) effect);
return;
}
if (mEffects == null) {
mEffects = new ArrayList<>();
}
mEffects.add((T) effect);
}
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_START) {
if (mEffects != null && !mEffects.isEmpty()) {
List effects = mEffects;
mEffects = null;
if (effects.size() == 1) {
mHandler.handleEffect(effects.get(0));
} else {
mHandler.handleEffect(effects);
}
}
} else if (event == Lifecycle.Event.ON_DESTROY) {
cancel();
}
}
void cancel() {
mLifecycle.removeObserver(this);
mEffects = null;
}
}
@Override
protected void onCleared() {
super.onCleared();
for (Integer key : mKeyToHandler.keySet()) {
EffectHandlerWrapper effectHandlerWrapper = mKeyToHandler.get(key);
if (effectHandlerWrapper != null) {
effectHandlerWrapper.cancel();
}
}
mKeyToHandler.clear();
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentMapEffectHandler.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
public abstract class QMUIFragmentMapEffectHandler extends QMUIFragmentEffectHandler {
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/effect/QMUIFragmentResultEffectHandler.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.effect;
public abstract class QMUIFragmentResultEffectHandler extends QMUIFragmentEffectHandler {
@Override
public HandlePolicy provideHandlePolicy() {
return HandlePolicy.NextStartEvent;
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/DefaultLatestVisitStorage.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class DefaultLatestVisitStorage implements QMUILatestVisitStorage {
private static final String SP_NAME = "qmui_latest_visit";
private static final String SP_FRAGMENT_RECORD_ID = "id_qmui_f_r";
private static final String SP_ACTIVITY_RECORD_ID = "id_qmui_a_r";
private static final String SP_ACTIVITY_ARG_PREFIX = "a_a_";
private static final String SP_FRAGMENT_ARG_PREFIX = "a_f_";
private static final char SP_INT_ARG_TAG = 'i';
private static final char SP_LONG_ARG_TAG = 'l';
private static final char SP_FLOAT_ARG_TAG = 'f';
private static final char SP_BOOLEAN_ARG_TAG = 'b';
private static final char SP_STRING_ARG_TAG = 's';
private SharedPreferences sp;
public DefaultLatestVisitStorage(Context context) {
sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
}
@Override
public int getFragmentRecordId() {
return sp.getInt(SP_FRAGMENT_RECORD_ID, NOT_EXIST);
}
@Nullable
@Override
public Map getFragmentArguments() {
HashMap ret = new HashMap<>();
for (Map.Entry entity : sp.getAll().entrySet()) {
String key = entity.getKey();
Object value = entity.getValue();
String prefix = SP_FRAGMENT_ARG_PREFIX;
if (key.startsWith(prefix)) {
char tag = key.charAt(prefix.length());
String realKey = key.substring(prefix.length() + 1);
if (tag == SP_INT_ARG_TAG) {
ret.put(realKey, new RecordArgumentEditor.Argument(value, Integer.TYPE));
} else if (tag == SP_BOOLEAN_ARG_TAG) {
ret.put(realKey, new RecordArgumentEditor.Argument(value, Boolean.TYPE));
} else if (tag == SP_LONG_ARG_TAG) {
ret.put(realKey, new RecordArgumentEditor.Argument(value, Long.TYPE));
} else if (tag == SP_FLOAT_ARG_TAG) {
ret.put(realKey, new RecordArgumentEditor.Argument(value, Float.TYPE));
} else if (tag == SP_STRING_ARG_TAG) {
ret.put(realKey, new RecordArgumentEditor.Argument(value, String.class));
}
}
}
return ret;
}
@Override
public int getActivityRecordId() {
return sp.getInt(SP_ACTIVITY_RECORD_ID, NOT_EXIST);
}
@Override
public void getAndWriteActivityArgumentsToIntent(@NonNull Intent intent) {
for (Map.Entry entity : sp.getAll().entrySet()) {
String key = entity.getKey();
Object value = entity.getValue();
String prefix = SP_ACTIVITY_ARG_PREFIX;
if (key.startsWith(prefix)) {
char tag = key.charAt(prefix.length());
String realKey = key.substring(prefix.length() + 1);
if (tag == SP_INT_ARG_TAG) {
intent.putExtra(realKey, (Integer) value);
} else if (tag == SP_BOOLEAN_ARG_TAG) {
intent.putExtra(realKey, (Boolean) value);
} else if (tag == SP_LONG_ARG_TAG) {
intent.putExtra(realKey, (Long) value);
} else if (tag == SP_FLOAT_ARG_TAG) {
intent.putExtra(realKey, (Float) value);
} else if (tag == SP_STRING_ARG_TAG) {
intent.putExtra(realKey, (String) value);
}
}
}
}
@Override
public void clearFragmentStorage() {
SharedPreferences.Editor editor = sp.edit();
editor.remove(SP_FRAGMENT_RECORD_ID);
clearArgument(editor, SP_FRAGMENT_ARG_PREFIX);
editor.apply();
}
@Override
public void clearActivityStorage() {
SharedPreferences.Editor editor = sp.edit();
editor.remove(SP_ACTIVITY_RECORD_ID);
clearArgument(editor, SP_ACTIVITY_ARG_PREFIX);
editor.apply();
}
@Override
public void saveFragmentRecordInfo(int id, Map arguments) {
SharedPreferences.Editor editor = sp.edit();
editor.putInt(SP_FRAGMENT_RECORD_ID, id);
putArguments(editor, SP_FRAGMENT_ARG_PREFIX, arguments);
editor.apply();
}
@Override
public void saveActivityRecordInfo(int id, @Nullable Map arguments) {
SharedPreferences.Editor editor = sp.edit();
editor.putInt(SP_ACTIVITY_RECORD_ID, id);
putArguments(editor, SP_ACTIVITY_ARG_PREFIX, arguments);
editor.apply();
}
@Override
public void clearAll() {
SharedPreferences.Editor editor = sp.edit();
editor.clear();
editor.apply();
}
private void clearArgument(SharedPreferences.Editor editor, String prefix) {
for (String key : sp.getAll().keySet()) {
if (key.startsWith(prefix)) {
editor.remove(key);
}
}
}
private void putArguments(SharedPreferences.Editor editor,
String prefix, Map arguments) {
// clear first
clearArgument(editor, prefix);
if (arguments != null && arguments.size() > 0) {
for (String name : arguments.keySet()) {
RecordArgumentEditor.Argument argument = arguments.get(name);
if (argument != null) {
Class> type = argument.getType();
Object value = argument.getValue();
if (type == Integer.TYPE || type == Integer.class) {
editor.putInt(prefix + SP_INT_ARG_TAG + name, (Integer) value);
} else if (type == Boolean.TYPE || type == Boolean.class) {
editor.putBoolean(prefix + SP_BOOLEAN_ARG_TAG + name, (Boolean) value);
} else if (type == Float.TYPE || type == Float.class) {
editor.putFloat(prefix + SP_FLOAT_ARG_TAG + name, (Float) value);
} else if (type == Long.TYPE || type == Long.class) {
editor.putLong(prefix + SP_LONG_ARG_TAG + name, (Long) value);
} else if (type == String.class) {
editor.putString(prefix + SP_STRING_ARG_TAG + name, (String) value);
} else {
throw new RuntimeException(String.format(
"Not support the type: %s", type.getSimpleName()));
}
}
}
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/LatestVisitArgumentCollector.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
import com.qmuiteam.qmui.arch.QMUILatestVisit;
public interface LatestVisitArgumentCollector {
/**
* Called by {@link QMUILatestVisit} to collect argument value
* Notice: This is called before onResume. So It can not used to save data
* produced after fragment resumed.
* @param editor RecordArgumentEditor
*/
void onCollectLatestVisitArgument(RecordArgumentEditor editor);
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/QMUILatestVisitStorage.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
import android.content.Intent;
import android.os.Bundle;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public interface QMUILatestVisitStorage {
int NOT_EXIST = -1;
// Fragment stuff
void saveFragmentRecordInfo(int id, @Nullable Map arguments);
int getFragmentRecordId();
@Nullable
Map getFragmentArguments();
void clearFragmentStorage();
// Activity Stuff
void saveActivityRecordInfo(int id, @Nullable Map arguments);
int getActivityRecordId();
void getAndWriteActivityArgumentsToIntent(@NonNull Intent intent);
void clearActivityStorage();
void clearAll();
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/RecordArgumentEditor.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
import android.os.Bundle;
import java.util.Map;
import androidx.annotation.Nullable;
public interface RecordArgumentEditor {
RecordArgumentEditor putString(String key, @Nullable String value);
RecordArgumentEditor putInt(String key, int value);
RecordArgumentEditor putLong(String key, long value);
RecordArgumentEditor putFloat(String key, float value);
RecordArgumentEditor putBoolean(String key, boolean value);
RecordArgumentEditor put(String key, RecordArgumentEditor.Argument argument);
RecordArgumentEditor remove(String key);
RecordArgumentEditor clear();
Map getAll();
class Argument {
private Object value;
private Class> type;
public Argument(Object value, Class> type) {
this.value = value;
this.type = type;
}
public Object getValue() {
return value;
}
public Class> getType() {
return type;
}
public void putToBundle(Bundle bundle, String key){
if(type == Integer.TYPE){
bundle.putInt(key, (Integer)value);
}else if(type == Boolean.TYPE){
bundle.putBoolean(key, (Boolean) value);
}else if(type == Long.TYPE){
bundle.putLong(key, (Long) value);
}else if(type == Float.TYPE){
bundle.putFloat(key, (Float) value);
}else if(type == String.class){
bundle.putString(key, (String) value);
}
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/RecordArgumentEditorImpl.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class RecordArgumentEditorImpl implements RecordArgumentEditor {
private HashMap mMap = new HashMap<>();
@Override
public synchronized RecordArgumentEditor putString(String key, @Nullable String value) {
mMap.put(key, new Argument(value, String.class));
return this;
}
@Override
public synchronized RecordArgumentEditor putInt(String key, int value) {
mMap.put(key, new Argument(value, Integer.TYPE));
return this;
}
@Override
public synchronized RecordArgumentEditor putLong(String key, long value) {
mMap.put(key, new Argument(value, Long.TYPE));
return this;
}
@Override
public synchronized RecordArgumentEditor putFloat(String key, float value) {
mMap.put(key, new Argument(value, Float.TYPE));
return this;
}
@Override
public synchronized RecordArgumentEditor putBoolean(String key, boolean value) {
mMap.put(key, new Argument(value, Boolean.TYPE));
return this;
}
@Override
public RecordArgumentEditor put(String key, Argument argument) {
mMap.put(key, argument);
return this;
}
@Override
public synchronized RecordArgumentEditor remove(String key) {
mMap.remove(key);
return this;
}
@Override
public synchronized RecordArgumentEditor clear() {
mMap.clear();
return this;
}
@Override
public Map getAll() {
return new HashMap<>(mMap);
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/record/RecordIdClassMap.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.record;
public interface RecordIdClassMap {
Class> getRecordClassById(int id);
int getIdByRecordClass(Class> clazz);
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/ActivitySchemeItem.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.util.ArrayMap
import com.qmuiteam.qmui.QMUILog
private val factories by lazy { ArrayMap, QMUISchemeIntentFactory>() }
internal class ActivitySchemeItem(
private val activityClass: Class,
useRefreshIfMatchedCurrent: Boolean,
private val intentFactoryCls: Class?,
required: ArrayMap?,
keysForInt: Array?,
keysForBool: Array?,
keysForLong: Array?,
keysForFloat: Array?,
keysForDouble: Array?,
defaultParams: Array?,
schemeMatcherCls: Class?,
schemeValueConverterCls: Class?
) : SchemeItem(
required, useRefreshIfMatchedCurrent, keysForInt, keysForBool,
keysForLong, keysForFloat, keysForDouble, defaultParams, schemeMatcherCls, schemeValueConverterCls
) {
override fun handle(
handler: QMUISchemeHandler,
handleContext: SchemeHandleContext,
schemeInfo: SchemeInfo
): Boolean {
var factoryCls = intentFactoryCls
if (factoryCls == null) {
factoryCls = handler.defaultIntentFactory
}
var factory = factories[factoryCls]
if (factory == null) {
try {
factory = factoryCls.newInstance()
factories[factoryCls] = factory
} catch (e: Exception) {
QMUILog.printErrStackTrace(
QMUISchemeHandler.TAG, e, "error to instance QMUISchemeIntentFactory: %d",
factoryCls.simpleName
)
}
}
if (factory != null) {
val params = convertFrom(schemeInfo.params)
if (factory.shouldBlockJump(handleContext.activity, activityClass, params)) {
return false
}
val intent = factory.factory(handleContext.activity, activityClass, params, schemeInfo.origin)
if (handleContext.canUseRefresh() &&
isUseRefreshIfMatchedCurrent &&
activityClass == handleContext.activity::class.java &&
handleContext.activity is ActivitySchemeRefreshable
) {
(handleContext.activity as ActivitySchemeRefreshable).refreshFromScheme(intent)
} else {
if (intent == null) {
return false
}
handleContext.pushActivity(activityClass, intent, factory)
if (shouldFinishCurrent(params)) {
handleContext.shouldFinishCurrent = true
}
}
return true
}
return false
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/FragmentSchemeItem.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.content.Intent
import android.util.ArrayMap
import com.qmuiteam.qmui.QMUILog
import com.qmuiteam.qmui.arch.QMUIFragment
import com.qmuiteam.qmui.arch.QMUIFragmentActivity
import com.qmuiteam.qmui.arch.annotation.FragmentContainerParam
private val factories by lazy {
mutableMapOf, QMUISchemeFragmentFactory>()
}
internal class FragmentSchemeItem(
private val fragmentCls: Class,
useRefreshIfMatchedCurrent: Boolean,
private val activityClsList: Array>,
private val fragmentFactoryCls: Class?,
private val forceNewActivity: Boolean,
required: ArrayMap?,
keysForInt: Array?,
keysForBool: Array?,
keysForLong: Array?,
keysForFloat: Array?,
keysForDouble: Array?,
defaultParams: Array?,
schemeMatcherCls: Class?,
schemeValueConverterCls: Class?
) : SchemeItem(
required, useRefreshIfMatchedCurrent, keysForInt, keysForBool, keysForLong,
keysForFloat, keysForDouble, defaultParams, schemeMatcherCls, schemeValueConverterCls
) {
override fun handle(
handler: QMUISchemeHandler,
handleContext: SchemeHandleContext,
schemeInfo: SchemeInfo
): Boolean {
if (activityClsList.isEmpty()) {
QMUILog.d(QMUISchemeHandler.TAG, "Can not start a new fragment because the host is't provided")
return false
}
var factoryCls = fragmentFactoryCls
if (factoryCls == null) {
factoryCls = handler.defaultFragmentFactory
}
var factory = factories[factoryCls]
if (factory == null) {
try {
factory = factoryCls.newInstance()
factories[factoryCls] = factory
} catch (e: Exception) {
QMUILog.printErrStackTrace(
QMUISchemeHandler.TAG, e,
"error to instance QMUISchemeFragmentFactory: %d", factoryCls.simpleName
)
}
}
if (factory == null) {
return false
}
val params = convertFrom(schemeInfo.params)
if (factory.shouldBlockJump(handleContext.activity, fragmentCls, params)) {
return false
}
val bundle = factory.factory(params, schemeInfo.origin)
if (!isCurrentActivityCanStartFragment(handleContext, params) || isForceNewActivity(params)) {
val ret = handleContext.flushAndBuildFirstFragment(activityClsList, params, FragmentAndArg(fragmentCls, bundle, factory))
if (ret) {
if (shouldFinishCurrent(params)) {
handleContext.shouldFinishCurrent = true
}
return true
}
return false
}
if (handleContext.canUseRefresh() && isUseRefreshIfMatchedCurrent) {
val fragmentActivity = handleContext.activity as QMUIFragmentActivity
val currentFragment = fragmentActivity.currentFragment
if (currentFragment != null && currentFragment.javaClass == fragmentCls && currentFragment is FragmentSchemeRefreshable) {
currentFragment.refreshFromScheme(bundle)
return true
}
}
handleContext.pushFragment(FragmentAndArg(fragmentCls, bundle, factory))
if (shouldFinishCurrent(params)) {
handleContext.shouldFinishCurrent = true
}
return true
}
private fun isCurrentActivityCanStartFragment(handleContext: SchemeHandleContext, scheme: Map?): Boolean {
if (handleContext.intentList.isNotEmpty() || handleContext.buildingIntent != null) {
if (!QMUIFragmentActivity::class.java.isAssignableFrom(handleContext.buildingActivityClass)) {
return false
}
val buildingIntent = handleContext.buildingIntent ?: return false
for (cls in activityClsList) {
if (isCurrentActivityCanStartFragment(
handleContext.buildingActivityClass,
buildingIntent,
cls,
scheme
)
) {
return true
}
}
return false
}
if (handleContext.activity !is QMUIFragmentActivity) {
return false
}
if (handleContext.activity.supportFragmentManager.isStateSaved) {
// use new activity if the state has already been saved.
return false
}
for (cls in activityClsList) {
if (isCurrentActivityCanStartFragment(
handleContext.buildingActivityClass,
handleContext.activity.intent,
cls,
scheme
)
) {
return true
}
}
return false
}
private fun isCurrentActivityCanStartFragment(
buildingActivity: Class,
buildingIntent: Intent,
targetActivity: Class,
scheme: Map?
): Boolean {
if (!targetActivity.isAssignableFrom(buildingActivity)) {
return false
}
val fragmentContainerParam = targetActivity.getAnnotation(FragmentContainerParam::class.java) ?: return true
val required: Array = fragmentContainerParam.required
val any: Array = fragmentContainerParam.any
if (required.isEmpty() && any.isEmpty()) {
return true
}
if (scheme == null || scheme.isEmpty()) {
return false
}
for (s in required) {
val value = scheme[s]
if (value == null || !buildingIntent.hasExtra(s)) {
return false
}
if (value.type == java.lang.Boolean.TYPE) {
if (buildingIntent.getBooleanExtra(s, false) != value.value as Boolean) {
return false
}
} else if (value.type == Integer.TYPE) {
if (buildingIntent.getIntExtra(s, 0) != value.value as Int) {
return false
}
} else if (value.type == java.lang.Long.TYPE) {
if (buildingIntent.getLongExtra(s, 0) != value.value as Long) {
return false
}
} else if (value.type == java.lang.Float.TYPE) {
if (buildingIntent.getFloatExtra(s, 0f) != value.value as Float) {
return false
}
} else if (value.type == java.lang.Double.TYPE) {
if (buildingIntent.getDoubleExtra(s, 0.0) != value.value as Double) {
return false
}
} else if (buildingIntent.getStringExtra(s) != value.value) {
return false
}
}
for (s in any) {
if (buildingIntent.hasExtra(s)) {
val value = scheme[s] ?: return false
if (value.type == java.lang.Boolean.TYPE) {
if (buildingIntent.getBooleanExtra(s, false) != value.value as Boolean) {
return false
}
} else if (value.type == Integer.TYPE) {
if (buildingIntent.getIntExtra(s, 0) != value.value as Int) {
return false
}
} else if (value.type == java.lang.Long.TYPE) {
if (buildingIntent.getLongExtra(s, 0) != value.value as Long) {
return false
}
} else if (value.type == java.lang.Float.TYPE) {
if (buildingIntent.getFloatExtra(s, 0f) != value.value as Float) {
return false
}
} else if (value.type == java.lang.Double.TYPE) {
if (buildingIntent.getDoubleExtra(s, 0.0) != value.value as Double) {
return false
}
} else if (buildingIntent.getStringExtra(s) != value.value) {
return false
}
}
}
return true
}
private fun isForceNewActivity(scheme: Map?): Boolean {
if (forceNewActivity) {
return true
}
if (scheme == null || scheme.isEmpty()) {
return false
}
val schemeValue = scheme[QMUISchemeHandler.ARG_FORCE_TO_NEW_ACTIVITY]
return schemeValue != null && schemeValue.type == java.lang.Boolean.TYPE && (schemeValue.value as Boolean)
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeBuilder.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.net.Uri
import android.util.ArrayMap
import java.util.*
class QMUISchemeBuilder(
private val prefix: String,
private val action: String,
private val encodeParams: Boolean
) {
companion object {
fun from(prefix: String, action: String, params: String?, encodeNewParams: Boolean): QMUISchemeBuilder {
val builder = QMUISchemeBuilder(prefix, action, encodeNewParams)
val paramsMap = HashMap()
parseParamsToMap(params, paramsMap)
if (paramsMap.isNotEmpty()) {
builder.params.putAll(paramsMap)
}
return builder
}
}
private val params = ArrayMap()
fun param(name: String, value: String): QMUISchemeBuilder {
if (encodeParams) {
params[name] = Uri.encode(value)
} else {
params[name] = value
}
return this
}
fun param(name: String, value: Int): QMUISchemeBuilder {
params[name] = value.toString()
return this
}
fun param(name: String, value: Boolean): QMUISchemeBuilder {
params[name] = if (value) "1" else "0"
return this
}
fun param(name: String, value: Long): QMUISchemeBuilder {
params[name] = value.toString()
return this
}
fun param(name: String, value: Float): QMUISchemeBuilder {
params[name] = value.toString()
return this
}
fun param(name: String, value: Double): QMUISchemeBuilder {
params[name] = value.toString()
return this
}
fun finishCurrent(finishCurrent: Boolean): QMUISchemeBuilder {
params[QMUISchemeHandler.ARG_FINISH_CURRENT] = if (finishCurrent) "1" else "0"
return this
}
fun forceToNewActivity(forceNew: Boolean): QMUISchemeBuilder {
params[QMUISchemeHandler.ARG_FORCE_TO_NEW_ACTIVITY] = if (forceNew) "1" else "0"
return this
}
fun build(): String {
val builder = StringBuilder()
builder.append(prefix)
builder.append(action)
builder.append("?")
for (i in 0 until params.size) {
if (i != 0) {
builder.append("&")
}
builder.append(params.keyAt(i))
builder.append("=")
builder.append(params.valueAt(i))
}
return builder.toString()
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeFragmentFactory.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.qmuiteam.qmui.QMUILog
import com.qmuiteam.qmui.arch.QMUIFragment
import com.qmuiteam.qmui.arch.QMUIFragmentActivity
import com.qmuiteam.qmui.arch.R
interface QMUISchemeFragmentFactory {
fun factory(fragmentCls: Class, bundle: Bundle?): QMUIFragment?
fun factory(scheme: Map?, origin: String): Bundle?
fun proxy(intent: Intent): Intent
fun startActivities(activity: Activity, intent: List, schemeInfo: List)
fun startFragmentAndDestroyCurrent(activity: QMUIFragmentActivity, fragment: QMUIFragment, schemeInfo: SchemeInfo): Int
fun startFragment(activity: QMUIFragmentActivity, fragment: List, schemeInfo: List): Int
fun shouldBlockJump(
activity: Activity,
fragmentCls: Class,
scheme: Map?
): Boolean
}
open class QMUIDefaultSchemeFragmentFactory : QMUISchemeFragmentFactory {
override fun factory(
fragmentCls: Class,
bundle: Bundle?
): QMUIFragment? {
return try {
val fragment = fragmentCls.newInstance()
fragment.arguments = bundle
fragment
} catch (e: Exception) {
QMUILog.printErrStackTrace(
QMUISchemeHandler.TAG, e,
"Error to create fragment: %s", fragmentCls.simpleName
)
null
}
}
override fun factory(scheme: Map?, origin: String): Bundle? {
val bundle = Bundle()
bundle.putBoolean(QMUISchemeHandler.ARG_FROM_SCHEME, true)
bundle.putString(QMUISchemeHandler.ARG_ORIGIN_SCHEME, origin)
if (scheme != null && scheme.isNotEmpty()) {
for ((name, schemeValue) in scheme) {
when (schemeValue.type) {
Integer.TYPE -> bundle.putInt(name, schemeValue.value as Int)
java.lang.Boolean.TYPE -> bundle.putBoolean(name, schemeValue.value as Boolean)
java.lang.Long.TYPE -> bundle.putLong(name, schemeValue.value as Long)
java.lang.Float.TYPE -> bundle.putFloat(name, schemeValue.value as Float)
java.lang.Double.TYPE -> bundle.putDouble(name, schemeValue.value as Double)
else -> bundle.putString(name, schemeValue.origin)
}
}
}
return bundle
}
override fun proxy(intent: Intent): Intent {
return intent
}
override fun startActivities(activity: Activity, intent: List, schemeInfo: List) {
if (intent.size == 1) {
activity.startActivity(intent[0])
} else {
activity.startActivities(intent.toTypedArray())
}
}
override fun startFragmentAndDestroyCurrent(activity: QMUIFragmentActivity, fragment: QMUIFragment, schemeInfo: SchemeInfo): Int {
return activity.startFragmentAndDestroyCurrent(fragment, true)
}
override fun startFragment(activity: QMUIFragmentActivity, fragment: List, schemeInfo: List): Int {
return activity.startFragments(fragment)
}
override fun shouldBlockJump(
activity: Activity,
fragmentCls: Class,
scheme: Map?
): Boolean {
return false
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandler.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import com.qmuiteam.qmui.QMUILog
import com.qmuiteam.qmui.arch.QMUIFragmentActivity
import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager
import java.util.*
class QMUISchemeHandler private constructor(builder: Builder) {
companion object {
const val TAG = "QMUISchemeHandler"
const val ARG_FROM_SCHEME = "__qmui_arg_from_scheme"
const val ARG_ORIGIN_SCHEME = "__qmui_arg_origin_scheme"
const val ARG_FORCE_TO_NEW_ACTIVITY = "__qmui_force_to_new_activity"
const val ARG_FINISH_CURRENT = "__qmui_finish_current"
private var sSchemeMap: SchemeMap? = null
init {
try {
val cls = Class.forName(SchemeMap::class.java.name + "Impl")
sSchemeMap = cls.newInstance() as SchemeMap
} catch (e: ClassNotFoundException) {
sSchemeMap = object : SchemeMap {
override fun findScheme(handler: QMUISchemeHandler, schemeAction: String, params: Map?): SchemeItem? {
return null
}
override fun exists(handler: QMUISchemeHandler, schemeAction: String): Boolean {
return false
}
}
} catch (e: IllegalAccessException) {
throw RuntimeException(
"Can not access the Class SchemeMapImpl. " +
"Please file a issue to report this."
)
} catch (e: InstantiationException) {
throw RuntimeException(
"Can not instance the Class SchemeMapImpl. " +
"Please file a issue to report this."
)
}
}
}
val prefix: String = builder.prefix
private var interpolatorList: List = builder.interceptorList
private val blockSameSchemeTimeout = builder.blockSameSchemeTimeout
val defaultIntentFactory = builder.defaultIntentFactory
val defaultFragmentFactory = builder.defaultFragmentFactory
val defaultSchemeMatcher = builder.defaultSchemeMatcher
private val fallbackInterceptor = builder.fallbackInterceptor
private val unKnownSchemeHandler = builder.unKnownSchemeHandler
private var lastHandledScheme: List? = null
private var lastSchemeHandledTime: Long = 0
fun getSchemeItem(action: String, params: Map?): SchemeItem? {
return sSchemeMap?.findScheme(this, action, params)
}
fun handle(scheme: String): Boolean {
val list = ArrayList(1)
list.add(scheme)
return handleSchemes(list)
}
fun handleSchemes(schemes: List): Boolean {
if (schemes.isEmpty()) {
return false
}
for (scheme in schemes) {
if (!scheme.startsWith(prefix)) {
return false
}
}
if (schemes == lastHandledScheme && System.currentTimeMillis() - lastSchemeHandledTime < blockSameSchemeTimeout) {
return true
}
val currentActivity = QMUISwipeBackActivityManager.getInstance().currentActivity ?: return false
val schemeInfoList = ArrayList(schemes.size)
for (schemeParam in schemes) {
val scheme = schemeParam.substring(prefix.length)
val elements: Array = scheme.split("\\?".toRegex()).toTypedArray()
val action = elements[0]
if (elements.isEmpty() || action == null || action.isEmpty()) {
return false
}
val params = mutableMapOf()
if (elements.size > 1) {
parseParamsToMap(elements[1], params)
}
schemeInfoList.add(SchemeInfo(action, params, scheme))
}
var handled = false
if (interpolatorList.isNotEmpty()) {
for (interpolator in interpolatorList) {
if (interpolator.intercept(this, currentActivity, schemeInfoList)) {
handled = true
break
}
}
}
if (!handled) {
var failed = false
val handleContext = SchemeHandleContext(currentActivity)
for (schemeInfo in schemeInfoList) {
val schemeItem = sSchemeMap!!.findScheme(this, schemeInfo.action, schemeInfo.params)
if (schemeItem == null) {
QMUILog.i(TAG, "findScheme failed: ${schemeInfo.origin}")
if(unKnownSchemeHandler != null && unKnownSchemeHandler.handle(this, handleContext, schemeInfo)){
continue
}
failed = true
break
}
schemeItem.appendDefaultParams(schemeInfo.params)
if (!schemeItem.handle(this, handleContext, schemeInfo)) {
QMUILog.i(TAG, "handle scheme failed: ${schemeInfo.origin}")
failed = true
break
}
}
if (!failed) {
val fragmentList = handleContext.fragmentList
val buildingIntent = handleContext.buildingIntent
if (handleContext.intentList.isEmpty() && buildingIntent == null) {
val fragments = fragmentList.mapNotNull {
it.factory.factory(it.fragmentClass, it.arg)
}
if (fragments.size == fragmentList.size) {
if (handleContext.shouldFinishCurrent) {
if (fragmentList.size == 1) {
fragmentList.last().factory.startFragmentAndDestroyCurrent(
handleContext.activity as QMUIFragmentActivity, fragments[0], schemeInfoList[0]
)
handled = true
} else {
QMUILog.e(TAG, "startFragmentAndDestroyCurrent not support muti fragments")
}
} else {
val commitId =
fragmentList.last().factory.startFragment(handleContext.activity as QMUIFragmentActivity, fragments, schemeInfoList)
handled = commitId >= 0
}
}
} else {
handled = handleContext.startActivities(schemeInfoList)
if (handled && handleContext.shouldFinishCurrent) {
handleContext.activity.finish()
}
}
}
}
if (!handled && fallbackInterceptor != null) {
handled = fallbackInterceptor.intercept(this, currentActivity, schemeInfoList)
}
if (handled) {
lastHandledScheme = schemes
lastSchemeHandledTime = System.currentTimeMillis()
}
return handled
}
class Builder(val prefix: String) {
val interceptorList = mutableListOf()
var blockSameSchemeTimeout = BLOCK_SAME_SCHEME_DEFAULT_TIMEOUT
var defaultIntentFactory: Class = QMUIDefaultSchemeIntentFactory::class.java
var defaultFragmentFactory: Class = QMUIDefaultSchemeFragmentFactory::class.java
var defaultSchemeMatcher: Class = QMUIDefaultSchemeMatcher::class.java
var unKnownSchemeHandler: QMUIUnknownSchemeHandler? = null
var fallbackInterceptor: QMUISchemeHandlerInterceptor? = null
fun addInterceptor(interceptor: QMUISchemeHandlerInterceptor) {
interceptorList.add(interceptor)
}
fun build(): QMUISchemeHandler {
return QMUISchemeHandler(this)
}
companion object {
const val BLOCK_SAME_SCHEME_DEFAULT_TIMEOUT: Long = 500
}
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeHandlerInterceptor.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.net.Uri
fun interface QMUISchemeHandlerInterceptor {
fun intercept(
schemeHandler: QMUISchemeHandler,
activity: Activity,
schemes: List
): Boolean
}
class QMUISchemeParamValueDecoder : QMUISchemeHandlerInterceptor {
override fun intercept(
schemeHandler: QMUISchemeHandler,
activity: Activity,
schemes: List
): Boolean {
for (scheme in schemes) {
for ((key, value) in scheme.params) {
if (value.isNotBlank()) {
scheme.params[key] = Uri.decode(value)
}
}
}
return false
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeIntentFactory.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.content.Intent
interface QMUISchemeIntentFactory {
fun factory(
activity: Activity,
activityClass: Class,
scheme: Map?,
origin: String
): Intent?
fun startActivities(
activity: Activity,
intent: List,
schemeInfo: List
)
fun shouldBlockJump(
activity: Activity,
activityClass: Class,
scheme: Map?
): Boolean
}
open class QMUIDefaultSchemeIntentFactory : QMUISchemeIntentFactory {
override fun factory(
activity: Activity,
activityClass: Class,
scheme: Map?,
origin: String
): Intent {
val intent = Intent(activity, activityClass)
intent.putExtra(QMUISchemeHandler.ARG_FROM_SCHEME, true)
intent.putExtra(QMUISchemeHandler.ARG_ORIGIN_SCHEME, origin)
if (scheme != null && scheme.isNotEmpty()) {
for ((name, schemeValue) in scheme) {
when (schemeValue.type) {
Integer.TYPE -> intent.putExtra(name, schemeValue.value as Int)
java.lang.Boolean.TYPE -> intent.putExtra(name, schemeValue.value as Boolean)
java.lang.Long.TYPE -> intent.putExtra(name, schemeValue.value as Long)
java.lang.Float.TYPE -> intent.putExtra(name, schemeValue.value as Float)
java.lang.Double.TYPE -> intent.putExtra(name, schemeValue.value as Double)
else -> intent.putExtra(name, schemeValue.origin)
}
}
}
return intent
}
override fun startActivities(activity: Activity, intent: List, schemeInfo: List) {
if (intent.size == 1) {
activity.startActivity(intent[0])
} else {
activity.startActivities(intent.toTypedArray())
}
}
override fun shouldBlockJump(
activity: Activity,
activityClass: Class,
scheme: Map?
): Boolean {
return false
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUISchemeMatcher.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
interface QMUISchemeMatcher {
fun match(schemeItem: SchemeItem, params: Map?): Boolean
}
open class QMUIDefaultSchemeMatcher : QMUISchemeMatcher {
override fun match(schemeItem: SchemeItem, params: Map?): Boolean {
return schemeItem.matchRequiredParam(params)
}
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/QMUIUnknownSchemeHandler.kt
================================================
package com.qmuiteam.qmui.arch.scheme
interface QMUIUnknownSchemeHandler {
fun handle(handler: QMUISchemeHandler, handleContext: SchemeHandleContext, schemeInfo: SchemeInfo): Boolean
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeHandleContext.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.qmuiteam.qmui.arch.QMUIFragment
import com.qmuiteam.qmui.arch.QMUIFragmentActivity
import com.qmuiteam.qmui.arch.annotation.FragmentContainerParam
import java.util.*
class SchemeHandleContext(val activity: Activity) {
val intentList: MutableList = ArrayList()
val fragmentList: MutableList = ArrayList()
var buildingIntent: Intent? = null
var buildingActivityClass: Class = activity::class.java
var shouldFinishCurrent = false
private var schemeIntentFactory: QMUISchemeIntentFactory? = null
private var schemeFragmentFactory: QMUISchemeFragmentFactory? = null
fun startActivities(schemeInfo: List): Boolean {
flushFragment()
if (intentList.isEmpty()) {
return false
}
intentList.forEachIndexed { index, intent ->
intent.putExtra(QMUIFragmentActivity.QMUI_MUTI_START_INDEX, index)
}
schemeFragmentFactory?.let {
it.startActivities(activity, intentList, schemeInfo)
return true
}
schemeIntentFactory?.let {
it.startActivities(activity, intentList, schemeInfo)
return true
}
return false
}
fun canUseRefresh(): Boolean {
return intentList.isEmpty() && fragmentList.isEmpty()
}
fun pushActivity(cls: Class, intent: Intent, factory: QMUISchemeIntentFactory) {
flushFragment()
intentList.add(intent)
schemeIntentFactory = factory
schemeFragmentFactory = null
buildingActivityClass = cls
}
private fun flushFragment() {
if (fragmentList.isNotEmpty()) {
val intent = buildingIntent ?: Intent(activity, buildingActivityClass).apply {
putExtras(activity.intent)
}.let {
fragmentList.first().factory.proxy(it)
}
val fragmentListArg = arrayListOf()
fragmentList.forEach {
fragmentListArg.add(Bundle().apply {
putString(QMUIFragmentActivity.QMUI_INTENT_DST_FRAGMENT_NAME, it.fragmentClass.name)
putBundle(QMUIFragmentActivity.QMUI_INTENT_FRAGMENT_ARG, it.arg)
})
}
intent.putParcelableArrayListExtra(QMUIFragmentActivity.QMUI_INTENT_FRAGMENT_LIST_ARG, fragmentListArg)
intentList.add(intent)
buildingIntent = null
fragmentList.clear()
}
}
fun flushAndBuildFirstFragment(
activityClsList: Array>,
params: Map?,
fragmentAndArg: FragmentAndArg
): Boolean {
flushFragment()
for (target in activityClsList) {
val intent = buildIntentForFragment(target, params)
if (intent != null) {
buildingIntent = fragmentAndArg.factory.proxy(intent)
buildingActivityClass = target
pushFragment(fragmentAndArg)
return true
}
}
return false
}
fun pushFragment(fragmentAndArg: FragmentAndArg) {
fragmentList.add(fragmentAndArg)
schemeIntentFactory = null
schemeFragmentFactory = fragmentAndArg.factory
}
private fun buildIntentForFragment(
activityCls: Class,
params: Map?
): Intent? {
val intent = Intent(activity, activityCls)
intent.putExtra(QMUISchemeHandler.ARG_FROM_SCHEME, true)
val fragmentContainerParam = activityCls.getAnnotation(FragmentContainerParam::class.java) ?: return intent
val required: Array = fragmentContainerParam.required
val any: Array = fragmentContainerParam.any
val optional: Array = fragmentContainerParam.optional
if (required.isEmpty() && any.isEmpty()) {
putOptionalSchemeValuesToIntent(intent, params, optional)
return intent
}
if (params == null || params.isEmpty()) {
// not matched.
return null
}
if (required.isNotEmpty()) {
for (arg in required) {
val value = params[arg] ?: return null // not matched.
putSchemeValueToIntent(intent, arg, value)
}
}
if (any.isNotEmpty()) {
var hasAny = false
for (arg in any) {
val value = params[arg]
if (value != null) {
putSchemeValueToIntent(intent, arg, value)
hasAny = true
}
}
if (!hasAny) {
return null
}
}
putOptionalSchemeValuesToIntent(intent, params, optional)
return intent
}
private fun putOptionalSchemeValuesToIntent(
intent: Intent,
scheme: Map?,
optional: Array
) {
if (scheme == null || scheme.isEmpty()) {
return
}
for (arg in optional) {
val value = scheme[arg]
value?.let { putSchemeValueToIntent(intent, arg, it) }
}
}
private fun putSchemeValueToIntent(intent: Intent, arg: String, value: SchemeValue) {
when (value.type) {
java.lang.Boolean.TYPE -> intent.putExtra(arg, value.value as Boolean)
Integer.TYPE -> intent.putExtra(arg, value.value as Int)
java.lang.Long.TYPE -> intent.putExtra(arg, value.value as Long)
java.lang.Float.TYPE -> intent.putExtra(arg, value.value as Float)
java.lang.Double.TYPE -> intent.putExtra(arg, value.value as Double)
else -> intent.putExtra(arg, value.origin)
}
}
}
class FragmentAndArg(
val fragmentClass: Class,
val arg: Bundle?,
val factory: QMUISchemeFragmentFactory
)
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeInfo.kt
================================================
package com.qmuiteam.qmui.arch.scheme
class SchemeInfo(
val action: String,
val params: MutableMap,
val origin: String
)
fun parseParamsToMap(schemeParams: String?, queryMap: MutableMap) {
if (schemeParams == null || schemeParams.isEmpty()) {
return
}
var start = 0
do {
val next = schemeParams.indexOf('&', start)
val end = if (next == -1) schemeParams.length else next
if (start == end) {
start += 1
continue
}
var separator = schemeParams.indexOf('=', start)
if (separator > end || separator == -1) {
separator = end
}
if (separator == start) {
start = end + 1
continue
}
val name = schemeParams.substring(start, separator)
val value = if (separator == end) "" else schemeParams.substring(separator + 1, end)
queryMap[name] = value
start = end + 1
} while (start < schemeParams.length)
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeItem.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.util.ArrayMap
import com.qmuiteam.qmui.QMUILog
import java.util.*
private val schemeMatchers by lazy {
HashMap, QMUISchemeMatcher>()
}
private val schemeValueConverters by lazy {
HashMap, QMUISchemeValueConverter>()
}
abstract class SchemeItem(
private val required: ArrayMap?,
val isUseRefreshIfMatchedCurrent: Boolean,
private val keysForInt: Array?,
private val keysForBool: Array?,
private val keysForLong: Array?,
private val keysForFloat: Array?,
private val keysForDouble: Array?,
private val defaultParams: Array?,
private val schemeMatcherCls: Class?,
private val schemeValueConverterCls: Class?
) {
fun appendDefaultParams(schemeParams: MutableMap?) {
if(schemeParams == null || defaultParams == null){
return
}
for (item in defaultParams) {
if (item.isNotEmpty()) {
val pair = item.split("=")
if (pair.size == 2) {
if(!schemeParams.contains(pair[0])){
schemeParams[pair[0]] = pair[1]
}
}
}
}
}
protected fun convertFrom(schemeParams: Map?): Map? {
if (schemeParams == null || schemeParams.isEmpty()) {
return null
}
val queryMap = mutableMapOf()
for ((name, value) in schemeParams) {
if (name.isEmpty()) {
continue
}
var usedValue = value
if (schemeValueConverterCls != null) {
var converter = schemeValueConverters[schemeValueConverterCls]
if (converter == null) {
try {
converter = schemeValueConverterCls.newInstance()
schemeValueConverters[schemeValueConverterCls] = converter
} catch (e: Exception) {
QMUILog.printErrStackTrace(
QMUISchemeHandler.TAG, e,
"error to instance QMUISchemeValueConverter: %d", schemeValueConverterCls.simpleName
)
}
}
if (converter != null) {
usedValue = converter.convert(name, value, schemeParams)
}
}
try {
when {
keysForInt?.contains(name) == true -> {
queryMap[name] = SchemeValue(usedValue, Integer.valueOf(usedValue), Integer.TYPE)
}
isBoolKey(name) -> {
queryMap[name] = SchemeValue(usedValue, convertStringToBool(usedValue), java.lang.Boolean.TYPE)
}
keysForLong?.contains(name) == true -> {
queryMap[name] = SchemeValue(usedValue, java.lang.Long.valueOf(usedValue), java.lang.Long.TYPE)
}
keysForFloat?.contains(name) == true -> {
queryMap[name] = SchemeValue(usedValue, java.lang.Float.valueOf(usedValue), java.lang.Float.TYPE)
}
keysForDouble?.contains(name) == true -> {
queryMap[name] = SchemeValue(usedValue, java.lang.Double.valueOf(usedValue), java.lang.Double.TYPE)
}
else -> {
queryMap[name] = SchemeValue(usedValue, usedValue, String::class.java)
}
}
} catch (e: Exception) {
QMUILog.printErrStackTrace(QMUISchemeHandler.TAG, e, "error to parse scheme param: %s = %s", name, value)
}
}
return queryMap
}
private fun isBoolKey(name: String): Boolean {
return QMUISchemeHandler.ARG_FORCE_TO_NEW_ACTIVITY == name || QMUISchemeHandler.ARG_FINISH_CURRENT == name ||
keysForBool?.contains(name) == true
}
private fun convertStringToBool(text: String?): Boolean {
return !(text.isNullOrBlank() || "0" == text || "false" == text.lowercase())
}
protected fun shouldFinishCurrent(scheme: Map?): Boolean {
if (scheme == null || scheme.isEmpty()) {
return false
}
val schemeValue = scheme[QMUISchemeHandler.ARG_FINISH_CURRENT]
return schemeValue != null && schemeValue.type == java.lang.Boolean.TYPE && schemeValue.value as Boolean
}
private fun getSchemeMatcher(handler: QMUISchemeHandler): QMUISchemeMatcher? {
var schemeMatcherCls = schemeMatcherCls
if (schemeMatcherCls == null) {
schemeMatcherCls = handler.defaultSchemeMatcher
}
var matcher = schemeMatchers[schemeMatcherCls]
if (matcher == null) {
try {
matcher = schemeMatcherCls.newInstance()
schemeMatchers[schemeMatcherCls] = matcher
} catch (e: Exception) {
QMUILog.printErrStackTrace(
QMUISchemeHandler.TAG, e,
"error to instance QMUISchemeMatcher: %d", schemeMatcherCls.simpleName
)
}
}
return matcher
}
// used by generated code(SchemeMapImpl)
fun match(handler: QMUISchemeHandler, params: Map?): Boolean {
val matcher = getSchemeMatcher(handler)
return matcher?.match(this, params) ?: matchRequiredParam(params)
}
fun matchRequiredParam(params: Map?): Boolean {
if (required == null || required.isEmpty()) {
return true
}
if (params == null || params.isEmpty()) {
return false
}
for (i in 0 until required.size) {
val key = required.keyAt(i)
if (!params.containsKey(key)) {
return false
}
val value = required.valueAt(i)
?: // if no value. that means scheme must provide this key.
continue
val actual = params[key]
if (actual == null || actual != value) {
return false
}
}
return true
}
abstract fun handle(handler: QMUISchemeHandler, handleContext: SchemeHandleContext, schemeInfo: SchemeInfo): Boolean
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeMap.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
interface SchemeMap {
fun findScheme(handler: QMUISchemeHandler, schemeAction: String, params: Map?): SchemeItem?
fun exists(handler: QMUISchemeHandler, schemeAction: String): Boolean
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeRefreshable.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
import android.content.Intent
import android.os.Bundle
interface ActivitySchemeRefreshable {
fun refreshFromScheme(intent: Intent?)
}
interface FragmentSchemeRefreshable {
fun refreshFromScheme(bundle: Bundle?)
}
================================================
FILE: arch/src/main/java/com/qmuiteam/qmui/arch/scheme/SchemeValue.kt
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.scheme
class SchemeValue(
val origin: String,
val value: Any,
val type: Class<*>
)
interface QMUISchemeValueConverter {
fun convert(key: String, originValue: String, schemeParams: Map?): String
}
class QMUIDefaultSchemeValueConverter : QMUISchemeValueConverter {
override fun convert(key: String, originValue: String, schemeParams: Map?): String {
return originValue
}
}
================================================
FILE: arch/src/main/res/anim/decelerate_factor_interpolator.xml
================================================
================================================
FILE: arch/src/main/res/anim/decelerate_low_factor_interpolator.xml
================================================
================================================
FILE: arch/src/main/res/anim/scale_enter.xml
================================================
================================================
FILE: arch/src/main/res/anim/scale_exit.xml
================================================
================================================
FILE: arch/src/main/res/anim/slide_in_left.xml
================================================
================================================
FILE: arch/src/main/res/anim/slide_in_right.xml
================================================
================================================
FILE: arch/src/main/res/anim/slide_out_left.xml
================================================
================================================
FILE: arch/src/main/res/anim/slide_out_right.xml
================================================
================================================
FILE: arch/src/main/res/anim/slide_still.xml
================================================
================================================
FILE: arch/src/main/res/anim/swipe_back_enter.xml
================================================
================================================
FILE: arch/src/main/res/anim/swipe_back_exit.xml
================================================
================================================
FILE: arch/src/main/res/anim/swipe_back_exit_still.xml
================================================
================================================
FILE: arch/src/main/res/animator/scale_enter.xml
================================================
================================================
FILE: arch/src/main/res/animator/scale_exit.xml
================================================
================================================
FILE: arch/src/main/res/animator/slide_in_left.xml
================================================
================================================
FILE: arch/src/main/res/animator/slide_in_right.xml
================================================
================================================
FILE: arch/src/main/res/animator/slide_out_left.xml
================================================
================================================
FILE: arch/src/main/res/animator/slide_out_right.xml
================================================
================================================
FILE: arch/src/main/res/animator/slide_still.xml
================================================
================================================
FILE: arch/src/main/res/values/attrs.xml
================================================
================================================
FILE: arch/src/main/res/values/ids.xml
================================================
================================================
FILE: arch/src/main/res/values/qmui_integers.xml
================================================
300
================================================
FILE: arch/src/main/res/values/strings.xml
================================================
arch
================================================
FILE: arch/src/main/res/values/style.xml
================================================
================================================
FILE: arch/src/test/java/com/qmuiteam/qmui/arch/ExampleUnitTest.java
================================================
package com.qmuiteam.qmui.arch;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see Testing documentation
*/
public class ExampleUnitTest {
}
================================================
FILE: arch-annotation/.gitignore
================================================
/build
================================================
FILE: arch-annotation/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
`java-library`
kotlin("jvm")
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.archVer
java {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
================================================
FILE: arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/ActivityScheme.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ActivityScheme {
String name();
String[] required() default {};
boolean useRefreshIfCurrentMatched() default false;
Class> customMatcher() default void.class;
Class> customFactory() default void.class;
String[] keysWithIntValue() default {};
String[] keysWithBoolValue() default {};
String[] keysWithLongValue() default {};
String[] keysWithFloatValue() default {};
String[] keysWithDoubleValue() default {};
String[] defaultParams() default {};
Class> valueConverter() default void.class;
}
================================================
FILE: arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentContainerParam.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* used for activity for different business.
*
*example:
*
* FragmentContainerParam(required = {"bookId"})
* class BookActivity extend QMUIFragmentActivity {
*
* }
*
* FragmentScheme(name = "bookDetail", activities = {BookActivity.class}, required={"bookId"})
* class BookDetailFragment extend QMUIFragment {
*
* }
*
* FragmentScheme(name = "bookRead", activities = {BookActivity.class}, required={"bookId"})
* class BookReadFragment extend QMUIFragment {
*
* }
*
* if bookId changed. QMUI will start up a new activity. so it's safe to put common book info
* in activityViewModel.
*
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FragmentContainerParam {
String[] required() default {};
String[] any() default {};
String[] optional() default {};
}
================================================
FILE: arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/FragmentScheme.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FragmentScheme {
String name();
Class>[] activities();
String[] required() default {};
boolean useRefreshIfCurrentMatched() default false;
Class> customMatcher() default void.class;
boolean forceNewActivity() default false;
String forceNewActivityKey() default "";
Class> customFactory() default void.class;
String[] keysWithIntValue() default {};
String[] keysWithBoolValue() default {};
String[] keysWithLongValue() default {};
String[] keysWithFloatValue() default {};
String[] keysWithDoubleValue() default {};
String[] defaultParams() default {};
Class> valueConverter() default void.class;
}
================================================
FILE: arch-annotation/src/main/java/com/qmuiteam/qmui/arch/annotation/LatestVisitRecord.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation can be used when you want to revert to last Fragment(Activity) that
* was visited before the app exited.
*
* if annotated for subclass of QMUIFragment, such as FragmentA, it must be annotated
* in the subclass of QMUIFragmentActivity, such as FragmentActivityA. FragmentActivityA
* must be annotated by FirstFragments or DefaultFirstFragment and the value must contain
* FragmentA.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LatestVisitRecord {
boolean onlyForDebug() default false;
}
================================================
FILE: arch-compiler/.gitignore
================================================
/build
================================================
FILE: arch-compiler/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
`java-library`
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.archVer
dependencies {
implementation(project(":arch-annotation"))
implementation(Dep.CodeGen.javapoet)
implementation(Dep.CodeGen.autoService)
annotationProcessor(Dep.CodeGen.autoService)
}
java {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
================================================
FILE: arch-compiler/src/main/java/com/qmuiteam/qmui/arch/BaseProcessor.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.WildcardTypeName;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import static javax.lang.model.element.ElementKind.INTERFACE;
public abstract class BaseProcessor extends AbstractProcessor {
static final String ACTIVITY_TYPE = "android.app.Activity";
static final String FRAGMENT_ACTIVITY_TYPE = "androidx.fragment.app.FragmentActivity";
static final String FRAGMENT_TYPE = "androidx.fragment.app.Fragment";
static final String QMUI_FRAGMENT_ACTIVITY_TYPE = "com.qmuiteam.qmui.arch.QMUIFragmentActivity";
static final String QMUI_FRAGMENT_TYPE = "com.qmuiteam.qmui.arch.QMUIFragment";
static final String QMUI_ACTIVITY_TYPE = "com.qmuiteam.qmui.arch.QMUIActivity";
static final ClassName QMUIFragmentActivityName = ClassName.get(
"com.qmuiteam.qmui.arch", "QMUIFragmentActivity");
static final ClassName QMUIFragmentName = ClassName.get(
"com.qmuiteam.qmui.arch", "QMUIFragment");
static ClassName MapName = ClassName.get("java.util", "Map");
static ClassName ListName = ClassName.get("java.util", "List");
static ClassName ArrayMapName = ClassName.get("android.util", "ArrayMap");
static ClassName ArrayListName = ClassName.get("java.util", "ArrayList");
static ClassName HashMapName = ClassName.get("java.util", "HashMap");
static ClassName IntegerName = ClassName.get("java.lang", "Integer");
static ClassName StringName = ClassName.get("java.lang", "String");
static ClassName OriginClassName = ClassName.get("java.lang", "Class");
static ParameterizedTypeName QMUIFragmentClassName = ParameterizedTypeName.get(
OriginClassName, WildcardTypeName.subtypeOf(QMUIFragmentName));
protected Filer mFiler;
protected Elements mElementUtils;
protected Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
protected ExecutableElement getOverrideMethod(ClassName creator, String methodName) {
TypeElement element = mElementUtils.getTypeElement(creator.toString());
List extends Element> elements = element.getEnclosedElements();
for (Element ele : elements) {
if (ele.getKind() != ElementKind.METHOD) continue;
if (methodName.equals(ele.getSimpleName().toString())) {
return (ExecutableElement) ele;
}
}
throw new RuntimeException(String.format("method %s of interface FirstFragmentFinder not found", methodName));
}
public void error(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.ERROR, element, message, args);
}
public void waring(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.WARNING, element, message, args);
}
public void note(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.NOTE, element, message, args);
}
private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
if (args.length > 0) {
message = String.format(message, args);
}
mMessager.printMessage(kind, message, element);
}
static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
if (isTypeEqual(typeMirror, otherType)) {
return true;
}
if (typeMirror.getKind() != TypeKind.DECLARED) {
return false;
}
DeclaredType declaredType = (DeclaredType) typeMirror;
List extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<');
for (int i = 0; i < typeArguments.size(); i++) {
if (i > 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
TypeMirror superType = typeElement.getSuperclass();
if (isSubtypeOfType(superType, otherType)) {
return true;
}
for (TypeMirror interfaceType : typeElement.getInterfaces()) {
if (isSubtypeOfType(interfaceType, otherType)) {
return true;
}
}
return false;
}
static boolean isTypeEqual(TypeMirror typeMirror, String otherType) {
return otherType.equals(typeMirror.toString());
}
static boolean isInterface(TypeMirror typeMirror) {
return typeMirror instanceof DeclaredType
&& ((DeclaredType) typeMirror).asElement().getKind() == INTERFACE;
}
static AnnotationMirror getAnnotationMirror(Element element, Class> annotation) {
List extends AnnotationMirror> list = element.getAnnotationMirrors();
if (list == null || list.isEmpty()) {
return null;
}
for (AnnotationMirror item : list) {
if (item.getAnnotationType().toString().equals(annotation.getName())) {
return item;
}
}
return null;
}
static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String key) {
Map extends ExecutableElement, ? extends AnnotationValue> map = annotationMirror.getElementValues();
for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> item : map.entrySet()) {
if (item.getKey().getSimpleName().toString().equals(key)) {
return item.getValue();
}
}
return null;
}
}
================================================
FILE: arch-compiler/src/main/java/com/qmuiteam/qmui/arch/LatestVisitProcessor.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import com.google.auto.service.AutoService;
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
@AutoService(Processor.class)
public class LatestVisitProcessor extends BaseProcessor {
private static ClassName RecordIdClassMap = ClassName.get(
"com.qmuiteam.qmui.arch.record", "RecordIdClassMap");
private static TypeName MapByClassName = ParameterizedTypeName.get(MapName,
OriginClassName, IntegerName);
private static TypeName MapByIdName = ParameterizedTypeName.get(MapName,
IntegerName, OriginClassName);
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(LatestVisitRecord.class);
if (elements.isEmpty()) {
return true;
}
TypeSpec.Builder classBuilder = TypeSpec
.classBuilder(RecordIdClassMap.simpleName() + "Impl")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(RecordIdClassMap);
classBuilder.addField(FieldSpec.builder(MapByClassName, "mClassToIdMap")
.addModifiers(Modifier.PRIVATE)
.build());
classBuilder.addField(FieldSpec.builder(MapByIdName, "mIdToClassMap")
.addModifiers(Modifier.PRIVATE)
.build());
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("mClassToIdMap = new $T<>()", HashMapName)
.addStatement("mIdToClassMap = new $T<>()", HashMapName);
HashMap hashCodes = new HashMap<>();
for (Element element : elements) {
if (element instanceof TypeElement) {
TypeElement classElement = (TypeElement) element;
TypeMirror elementType = classElement.asType();
boolean isFragmentActivity = isSubtypeOfType(elementType, QMUI_FRAGMENT_ACTIVITY_TYPE);
boolean isFragment = isSubtypeOfType(elementType, QMUI_FRAGMENT_TYPE);
boolean isActivity = isSubtypeOfType(elementType, QMUI_ACTIVITY_TYPE);
if (isFragmentActivity || isFragment || isActivity) {
ClassName elementName = ClassName.get(classElement);
String simpleName = elementName.simpleName();
int hashCode = simpleName.hashCode();
if(hashCodes.keySet().contains(hashCode)){
if(hashCodes.keySet().contains(hashCode)){
error(element, "The hashCode of " + simpleName + " conflict with "
+ hashCodes.get(hashCode) + "; Please consider changing the class name");
continue;
}
}
hashCodes.put(hashCode, simpleName);
constructorBuilder.addStatement("mClassToIdMap.put($T.class, $L)",
elementName,
hashCode);
constructorBuilder.addStatement("mIdToClassMap.put($L, $T.class)",
hashCode,
elementName);
} else {
error(element, "Must annotated on subclasses of QMUIFragmentActivity");
}
}
}
ExecutableElement iGetClassById = getOverrideMethod(
RecordIdClassMap, "getRecordClassById");
MethodSpec.Builder getRecordMetaById = MethodSpec.overriding(iGetClassById)
.addStatement("return mIdToClassMap.get($L)",
iGetClassById.getParameters().get(0).getSimpleName().toString());
ExecutableElement iGetIdByClass = getOverrideMethod(
RecordIdClassMap, "getIdByRecordClass");
MethodSpec.Builder getRecordMetaByClass = MethodSpec.overriding(iGetIdByClass)
.addStatement("return mClassToIdMap.get($L)",
iGetIdByClass.getParameters().get(0).getSimpleName().toString());
classBuilder
.addMethod(constructorBuilder.build())
.addMethod(getRecordMetaById.build())
.addMethod(getRecordMetaByClass.build());
try {
JavaFile.builder(RecordIdClassMap.packageName(), classBuilder.build())
.build().writeTo(mFiler);
} catch (IOException e) {
error(null, "Unable to generate RecordMetaMapImpl: %s", e.getMessage());
}
return true;
}
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(LatestVisitRecord.class.getCanonicalName());
return types;
}
}
================================================
FILE: arch-compiler/src/main/java/com/qmuiteam/qmui/arch/SchemeProcessor.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.arch;
import com.google.auto.service.AutoService;
import com.qmuiteam.qmui.arch.annotation.ActivityScheme;
import com.qmuiteam.qmui.arch.annotation.FragmentScheme;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeMirror;
@AutoService(Processor.class)
public class SchemeProcessor extends BaseProcessor {
private static String QMUISchemeIntentFactoryType = "com.qmuiteam.qmui.arch.scheme.QMUISchemeIntentFactory";
private static String QMUISchemeFragmentFactoryType = "com.qmuiteam.qmui.arch.scheme.QMUISchemeFragmentFactory";
private static String QMUISchemeMatcherType = "com.qmuiteam.qmui.arch.scheme.QMUISchemeMatcher";
private static String QMUISchemeValueConverterType = "com.qmuiteam.qmui.arch.scheme.QMUISchemeValueConverter";
private static ClassName SchemeMap = ClassName.get(
"com.qmuiteam.qmui.arch.scheme", "SchemeMap");
private static ClassName SchemeItem = ClassName.get(
"com.qmuiteam.qmui.arch.scheme", "SchemeItem");
private static ClassName ActivitySchemeItem = ClassName.get(
"com.qmuiteam.qmui.arch.scheme", "ActivitySchemeItem");
private static ClassName FragmentSchemeItem = ClassName.get(
"com.qmuiteam.qmui.arch.scheme", "FragmentSchemeItem");
private static TypeName SchemeItemList = ParameterizedTypeName.get(ListName, SchemeItem);
private static TypeName MapByAction = ParameterizedTypeName.get(MapName,
StringName, SchemeItemList);
private static TypeName MapForSchemeRequired = ParameterizedTypeName.get(ArrayMapName,
StringName, StringName);
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set extends Element> activitySchemes = roundEnv.getElementsAnnotatedWith(ActivityScheme.class);
Set extends Element> fragmentSchemes = roundEnv.getElementsAnnotatedWith(FragmentScheme.class);
if (activitySchemes.isEmpty() && fragmentSchemes.isEmpty()) {
return true;
}
TypeSpec.Builder classBuilder = TypeSpec
.classBuilder(SchemeMap.simpleName() + "Impl")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(SchemeMap);
classBuilder.addField(FieldSpec.builder(MapByAction, "mSchemeMap")
.addModifiers(Modifier.PRIVATE)
.build());
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("mSchemeMap = new $T<>()", HashMapName);
Map> schemeMap = new HashMap<>();
for (Element element : activitySchemes) {
if (element instanceof TypeElement) {
TypeElement classElement = (TypeElement) element;
TypeMirror elementType = classElement.asType();
boolean isActivity = isSubtypeOfType(elementType, ACTIVITY_TYPE);
if (!isActivity) {
error(element, "Must annotated on subclasses of Activity");
} else {
ActivityScheme annotation = element.getAnnotation(ActivityScheme.class);
String name = annotation.name();
List- elements = schemeMap.get(name);
if (elements == null) {
elements = new ArrayList<>();
schemeMap.put(name, elements);
}
elements.add(new Item(true, classElement, elementType, annotation.required()));
}
}
}
for (Element element : fragmentSchemes) {
if (element instanceof TypeElement) {
TypeElement classElement = (TypeElement) element;
TypeMirror elementType = classElement.asType();
boolean isQMUIFragment = isSubtypeOfType(elementType, QMUI_FRAGMENT_TYPE);
if (!isQMUIFragment) {
error(element, "Must annotated on subclasses of QMUIFragment");
} else {
FragmentScheme annotation = element.getAnnotation(FragmentScheme.class);
String name = annotation.name();
List
- elements = schemeMap.get(name);
if (elements == null) {
elements = new ArrayList<>();
schemeMap.put(name, elements);
}
elements.add(new Item(false, classElement, elementType, annotation.required()));
}
}
}
constructorBuilder.addStatement("$T elements", SchemeItemList);
constructorBuilder.addStatement("$T required = null", MapForSchemeRequired);
for (String key : schemeMap.keySet()) {
List
- items = schemeMap.get(key);
constructorBuilder.addStatement("elements = new $T<>()", ArrayListName);
items.sort(new Comparator
- () {
@Override
public int compare(Item item, Item t1) {
int c1 = item.getRequiredCount();
int c2 = t1.getRequiredCount();
return Integer.compare(c2, c1);
}
});
for (Item item : items) {
ClassName elementName = ClassName.get(item.element);
if (item.isActivity) {
ActivityScheme annotation = item.element.getAnnotation(ActivityScheme.class);
AnnotationMirror annotationMirror = getAnnotationMirror(item.element, ActivityScheme.class);
if (annotationMirror == null) {
continue;
}
appendRequired(constructorBuilder, annotation.required());
CodeBlock customFactory = generateCustomFactory(true, annotationMirror);
CodeBlock intParam = generateTypedParams(annotation.keysWithIntValue());
CodeBlock boolParam = generateTypedParams(annotation.keysWithBoolValue());
CodeBlock longParam = generateTypedParams(annotation.keysWithLongValue());
CodeBlock floatParam = generateTypedParams(annotation.keysWithFloatValue());
CodeBlock doubleParam = generateTypedParams(annotation.keysWithDoubleValue());
CodeBlock defaultParam = generateTypedParams(annotation.defaultParams());
CodeBlock customMatcher = generateCustomMatcher(annotationMirror);
CodeBlock valueConverter = generateValueInterceptor(annotationMirror);
CodeBlock codeBlock = CodeBlock.builder()
.add("elements.add(")
/**/.add("new $T(", ActivitySchemeItem)
/*---*/.add("$T.class", elementName)
/*---*/.add(",")
/*---*/.add("$L", annotation.useRefreshIfCurrentMatched())
/*---*/.add(",")
/*---*/.add(customFactory)
/*---*/.add(",")
/*---*/.add("required")
/*---*/.add(",")
/*---*/.add(intParam)
/*---*/.add(",")
/*---*/.add(boolParam)
/*---*/.add(",")
/*---*/.add(longParam)
/*---*/.add(",")
/*---*/.add(floatParam)
/*---*/.add(",")
/*---*/.add(doubleParam)
/*---*/.add(",")
/*---*/.add(defaultParam)
/*---*/.add(",")
/*---*/.add(customMatcher)
/*---*/.add(",")
/*---*/.add(valueConverter)
/**/.add(")")
.add(")")
.build();
constructorBuilder.addStatement(codeBlock);
} else {
FragmentScheme annotation = item.element.getAnnotation(FragmentScheme.class);
AnnotationMirror annotationMirror = getAnnotationMirror(item.element, FragmentScheme.class);
if (annotationMirror == null) {
continue;
}
appendRequired(constructorBuilder, annotation.required());
CodeBlock customFactory = generateCustomFactory(false, annotationMirror);
CodeBlock activities = generateFragmentHostActivityList(annotation);
CodeBlock intParam = generateTypedParams(annotation.keysWithIntValue());
CodeBlock boolParam = generateTypedParams(annotation.keysWithBoolValue());
CodeBlock longParam = generateTypedParams(annotation.keysWithLongValue());
CodeBlock floatParam = generateTypedParams(annotation.keysWithFloatValue());
CodeBlock doubleParam = generateTypedParams(annotation.keysWithDoubleValue());
CodeBlock defaultParam = generateTypedParams(annotation.defaultParams());
CodeBlock customMatcher = generateCustomMatcher(annotationMirror);
CodeBlock valueConverter = generateValueInterceptor(annotationMirror);
CodeBlock codeBlock = CodeBlock.builder()
.add("elements.add(")
/**/.add("new $T(", FragmentSchemeItem)
/*---*/.add("$T.class", elementName)
/*---*/.add(",")
/*---*/.add("$L", annotation.useRefreshIfCurrentMatched())
/*---*/.add(",")
/*---*/.add(activities)
/*---*/.add(",")
/*---*/.add(customFactory)
/*---*/.add(",")
/*---*/.add("$L", annotation.forceNewActivity())
/*---*/.add(",")
/*---*/.add("required")
/*---*/.add(",")
/*---*/.add(intParam)
/*---*/.add(",")
/*---*/.add(boolParam)
/*---*/.add(",")
/*---*/.add(longParam)
/*---*/.add(",")
/*---*/.add(floatParam)
/*---*/.add(",")
/*---*/.add(doubleParam)
/*---*/.add(",")
/*---*/.add(defaultParam)
/*---*/.add(",")
/*---*/.add(customMatcher)
/*---*/.add(",")
/*---*/.add(valueConverter)
/**/.add(")")
.add(")")
.build();
constructorBuilder.addStatement(codeBlock);
}
}
constructorBuilder.addStatement("mSchemeMap.put($S, elements)", key);
}
ExecutableElement findScheme = getOverrideMethod(
SchemeMap, "findScheme");
List extends VariableElement> findSchemeParams = findScheme.getParameters();
String schemeHandler = findSchemeParams.get(0).getSimpleName().toString();
String schemeAction = findSchemeParams.get(1).getSimpleName().toString();
String schemeParam = findSchemeParams.get(2).getSimpleName().toString();
MethodSpec.Builder getRecordMetaById = MethodSpec.overriding(findScheme)
.addStatement("$T list = mSchemeMap.get($L)", SchemeItemList, schemeAction)
.beginControlFlow("if(list == null || list.isEmpty())")
/**/.addStatement("return null")
.endControlFlow()
.beginControlFlow("for (int i = 0; i < list.size(); i++)")
/**/.addStatement("$T item = list.get(i)", SchemeItem)
/**/.beginControlFlow("if(item.match($L, $L))", schemeHandler, schemeParam)
/*--*/.addStatement("return item")
/**/.endControlFlow()
.endControlFlow()
.addStatement("return null");
ExecutableElement exists = getOverrideMethod(
SchemeMap, "exists");
MethodSpec.Builder getRecordMetaByClass = MethodSpec.overriding(exists)
.addStatement("return mSchemeMap.containsKey($L)", exists.getParameters().get(1).getSimpleName().toString());
classBuilder
.addMethod(constructorBuilder.build())
.addMethod(getRecordMetaById.build())
.addMethod(getRecordMetaByClass.build());
try {
JavaFile.builder(SchemeMap.packageName(), classBuilder.build())
.build().writeTo(mFiler);
} catch (IOException e) {
error(null, "Unable to generate RecordMetaMapImpl: %s", e.getMessage());
}
return true;
}
private void appendRequired(MethodSpec.Builder constructorBuilder, String[] required) {
if (required == null || required.length == 0) {
constructorBuilder.addStatement("required =null");
return;
}
constructorBuilder.addStatement("required = new $T<>()", ArrayMapName);
for (int i = 0; i < required.length; i++) {
String condition = required[i];
if (condition == null || condition.isEmpty()) {
continue;
}
int index = condition.indexOf("=");
if (index < 0 || index >= condition.length()) {
constructorBuilder.addStatement("required.put($S, null)", condition);
} else {
String key = condition.substring(0, index);
String value = index == condition.length() - 1 ? "" : condition.substring(index + 1);
constructorBuilder.addStatement("required.put($S, $S)", key, value);
}
}
}
private CodeBlock generateTypedParams(String[] keys) {
CodeBlock.Builder builder = CodeBlock.builder();
if (keys == null || keys.length == 0) {
builder.add("null");
} else {
builder.add("new $T[]{", StringName);
for (int i = 0; i < keys.length; i++) {
if (i != 0) {
builder.add(",");
}
builder.add("$S", keys[i]);
}
builder.add("}");
}
return builder.build();
}
private CodeBlock generateCustomFactory(boolean isActivity, AnnotationMirror annotationMirror){
AnnotationValue customFactory = getAnnotationValue(annotationMirror, "customFactory");
if (customFactory == null) {
return CodeBlock.of("null");
}
TypeMirror typeMirror = (TypeMirror) customFactory.getValue();
if(isActivity){
if (!isSubtypeOfType(typeMirror, QMUISchemeIntentFactoryType)) {
throw new IllegalStateException("customFactory must implement interface QMUISchemeIntentFactory.");
}
}else{
if (!isSubtypeOfType(typeMirror, QMUISchemeFragmentFactoryType)) {
throw new IllegalStateException("customFactory must implement interface QMUISchemeFragmentFactory.");
}
}
return CodeBlock.of("$T.class", typeMirror);
}
private CodeBlock generateCustomMatcher(AnnotationMirror annotationMirror){
AnnotationValue customFactory = getAnnotationValue(annotationMirror, "customMatcher");
if (customFactory == null) {
return CodeBlock.of("null");
}
TypeMirror typeMirror = (TypeMirror) customFactory.getValue();
if (!isSubtypeOfType(typeMirror, QMUISchemeMatcherType)) {
throw new IllegalStateException("customMatcher must implement interface QMUISchemeMatcher.");
}
return CodeBlock.of("$T.class", typeMirror);
}
private CodeBlock generateValueInterceptor(AnnotationMirror annotationMirror){
AnnotationValue valueConverter = getAnnotationValue(annotationMirror, "valueConverter");
if (valueConverter == null) {
return CodeBlock.of("null");
}
TypeMirror typeMirror = (TypeMirror) valueConverter.getValue();
if (!isSubtypeOfType(typeMirror, QMUISchemeValueConverterType)) {
throw new IllegalStateException("customMatcher must implement interface QMUISchemeMatcher.");
}
return CodeBlock.of("$T.class", typeMirror);
}
private CodeBlock generateFragmentHostActivityList(FragmentScheme fragmentScheme){
CodeBlock.Builder builder = CodeBlock.builder();
TypeMirror[] activities = null;
try {
fragmentScheme.activities();
} catch (MirroredTypesException mte) {
List extends TypeMirror> containerMirrors = mte.getTypeMirrors();
activities = new TypeMirror[containerMirrors.size()];
for (int i = 0; i < activities.length; i++) {
activities[i] = containerMirrors.get(i);
}
}
if(activities == null || activities.length == 0){
throw new IllegalStateException("FragmentScheme#activities can not be empty.");
}
builder.add("new $T[]{", OriginClassName);
for(int i=0; i < activities.length; i++){
TypeMirror item = activities[i];
if(!isSubtypeOfType(item, QMUI_FRAGMENT_ACTIVITY_TYPE)){
throw new IllegalStateException("FragmentScheme#activities must be QMUIFragmentActivity.");
}
if(i > 0){
builder.add(",");
}
builder.add("$T.class", ClassName.get(item));
}
builder.add("}");
return builder.build();
}
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(ActivityScheme.class.getCanonicalName());
types.add(FragmentScheme.class.getCanonicalName());
return types;
}
static class Item {
boolean isActivity = false;
TypeElement element;
TypeMirror type;
String[] required;
public Item(boolean isActivity, TypeElement element, TypeMirror type, String[] required) {
this.isActivity = isActivity;
this.element = element;
this.type = type;
this.required = required;
}
int getRequiredCount(){
return required == null ? 0 : required.length;
}
}
}
================================================
FILE: build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
import com.qmuiteam.plugin.Dep
buildscript {
repositories {
mavenCentral()
google()
mavenLocal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.2.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20")
}
}
plugins {
id("qmui-dep")
id("com.osacky.doctor") version "0.8.0"
}
subprojects {
group = Dep.QMUI.group
}
allprojects {
repositories {
mavenCentral()
google()
mavenLocal()
}
}
================================================
FILE: compiler/.gitignore
================================================
/build
/*.iml
================================================
FILE: compiler/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
`java-library`
}
java {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
dependencies {
implementation(project(":lib"))
implementation(Dep.CodeGen.javapoet)
implementation(Dep.CodeGen.autoService)
annotationProcessor(Dep.CodeGen.autoService)
}
================================================
FILE: compiler/src/main/java/com/qmuiteam/qmuidemo/compiler/WidgetProcessor.java
================================================
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmuidemo.compiler;
import com.google.auto.service.AutoService;
import com.qmuiteam.qmuidemo.lib.annotation.Widget;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
/**
* @author cginechen
* @date 2016-12-13
*/
@AutoService(Processor.class)
public class WidgetProcessor extends AbstractProcessor {
private Filer mFiler; //文件相关的辅助类
@SuppressWarnings("FieldCanBeLocal") private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类
private boolean mIsFileCreated = false;
private final String mClassName = "QDWidgetContainer";
private final String mPackageName = "com.qmuiteam.qmuidemo.manager";
ClassName mMapName = ClassName.get("java.util", "Map");
ClassName mHashMapName = ClassName.get("java.util", "HashMap");
ClassName mItemDescName = ClassName.get("com.qmuiteam.qmuidemo.model", "QDItemDescription");
ClassName mBaseFragmentName = ClassName.get("com.qmuiteam.qmuidemo.base", "BaseFragment");
TypeName mBaseFragmentClassName = ParameterizedTypeName.get(ClassName.get(Class.class),
WildcardTypeName.subtypeOf(mBaseFragmentName));
TypeName mMapFieldTypeName = ParameterizedTypeName.get(mMapName,
mBaseFragmentClassName, mItemDescName);
ClassName mWidgetContainerName = ClassName.get(mPackageName, mClassName);
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
mIsFileCreated = false;
}
/**
* @return 指定哪些注解应该被注解处理器注册
*/
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(Widget.class.getCanonicalName());
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (mIsFileCreated) {
return true;
}
mIsFileCreated = true;
TypeSpec.Builder widgetContainerBuilder = TypeSpec.classBuilder(mWidgetContainerName);
FieldSpec instanceField = FieldSpec.builder(mWidgetContainerName, "sInstance")
.addModifiers(Modifier.PRIVATE)
.addModifiers(Modifier.STATIC)
.initializer("new $T()", mWidgetContainerName)
.build();
FieldSpec mapField = FieldSpec.builder(mMapFieldTypeName, "mWidgets")
.addModifiers(Modifier.PRIVATE)
.build();
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addStatement("mWidgets = new $T<>()", mHashMapName);
for (Element element : roundEnvironment.getElementsAnnotatedWith(Widget.class)) {
if (element instanceof TypeElement) {
TypeElement classElement = (TypeElement) element;
ClassName elementName = ClassName.get(classElement);
Widget widget = classElement.getAnnotation(Widget.class);
String name = null;
// http://www.programcreek.com/java-api-examples/index.php?api=javax.lang.model.type.TypeMirror
// https://blog.retep.org/2009/02/13/getting-class-values-from-annotations-in-an-annotationprocessor/
try {
widget.widgetClass();
} catch (MirroredTypeException mte) {
TypeMirror nameMirror = mte.getTypeMirror();
if (nameMirror.getKind() == TypeKind.DECLARED) {
name = ((DeclaredType) nameMirror).asElement().getSimpleName().toString();
}
}
if (name == null && widget.name().length() > 0) {
name = widget.name();
}
if (name == null || name.length() == 0) {
error("please provide widgetClass or name");
}
constructorBuilder.addStatement("mWidgets.put($T.class, new $T($T.class, $S, $L, $S))",
elementName,
mItemDescName,
elementName,
name,
widget.iconRes(),
widget.docUrl());
}
}
MethodSpec constructorMethod = constructorBuilder.build();
MethodSpec instanceMethod = MethodSpec.methodBuilder("getInstance")
.addModifiers(Modifier.PUBLIC)
.addModifiers(Modifier.STATIC)
.returns(mWidgetContainerName)
.addStatement("return sInstance")
.build();
MethodSpec getMethod = MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC)
.returns(mItemDescName)
.addParameter(mBaseFragmentClassName, "fragment")
.addStatement("return mWidgets.get($L)", "fragment")
.build();
try {
widgetContainerBuilder
.addField(instanceField)
.addField(mapField)
.addMethod(constructorMethod)
.addMethod(instanceMethod)
.addMethod(getMethod);
JavaFile.builder(mPackageName, widgetContainerBuilder.build()).build().writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
private void error(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}
private void info(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(msg, args));
}
}
================================================
FILE: compose/.gitignore
================================================
/build
================================================
FILE: compose/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.composeVer
android {
compileSdk = Dep.compileSdk
buildFeatures {
compose = true
}
defaultConfig {
minSdk = Dep.minSdk
targetSdk = Dep.targetSdk
}
buildTypes {
getByName("release"){
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
kotlinOptions {
jvmTarget = Dep.kotlinJvmTarget
}
composeOptions {
kotlinCompilerExtensionVersion = Dep.Compose.version
}
}
dependencies {
api(project(":compose-core"))
api(Dep.Compose.constraintlayout)
}
================================================
FILE: compose/proguard-rules.pro
================================================
# 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
================================================
FILE: compose/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt
================================================
package com.qmuiteam.compose
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.qmuiteam.compose", appContext.packageName)
}
}
================================================
FILE: compose/src/main/AndroidManifest.xml
================================================
================================================
FILE: compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt
================================================
package com.qmuiteam.compose.modal
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import kotlinx.coroutines.flow.MutableStateFlow
internal abstract class QMUIModalPresent(
private val rootLayout: FrameLayout,
private val onBackPressedDispatcher: OnBackPressedDispatcher,
val mask: Color = DefaultMaskColor,
val systemCancellable: Boolean = true,
val maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
) : QMUIModal {
private val onShowListeners = arrayListOf()
private val onDismissListeners = arrayListOf()
private val visibleFlow = MutableStateFlow(false)
private var isShown = false
private var isDismissing = false
private val composeLayout = ComposeView(rootLayout.context).apply {
visibility = View.GONE
}
private val onBackPressedCallback = object : OnBackPressedCallback(systemCancellable) {
override fun handleOnBackPressed() {
dismiss()
}
}
init {
composeLayout.setContent {
Box(modifier = Modifier.fillMaxSize()) {
val visible by visibleFlow.collectAsState(initial = false)
ModalContent(visible = visible) {
if (isDismissing) {
doAfterDismiss()
}
}
}
}
}
private fun doAfterDismiss() {
isDismissing = false
composeLayout.visibility = View.GONE
composeLayout.disposeComposition()
rootLayout.removeView(composeLayout)
onBackPressedCallback.remove()
onDismissListeners.forEach {
it.invoke(this)
}
}
@Composable
abstract fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit)
override fun isShowing(): Boolean {
return isShown
}
override fun show(): QMUIModal {
if (isShown || isDismissing) {
return this
}
isShown = true
rootLayout.addView(composeLayout, generateLayoutParams())
composeLayout.visibility = View.VISIBLE
visibleFlow.value = true
onBackPressedDispatcher.addCallback(onBackPressedCallback)
onShowListeners.forEach {
it.invoke(this)
}
return this
}
open fun generateLayoutParams(): FrameLayout.LayoutParams {
return FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun dismiss() {
if (!isShown) {
return
}
isShown = false
isDismissing = true
visibleFlow.value = false
}
override fun doOnShow(listener: QMUIModal.Action): QMUIModal {
onShowListeners.add(listener)
return this
}
override fun doOnDismiss(listener: QMUIModal.Action): QMUIModal {
onDismissListeners.add(listener)
return this
}
override fun removeOnShowAction(listener: QMUIModal.Action): QMUIModal {
onShowListeners.remove(listener)
return this
}
override fun removeOnDismissAction(listener: QMUIModal.Action): QMUIModal {
onDismissListeners.remove(listener)
return this
}
}
internal class StillModalImpl(
rootLayout: FrameLayout,
onBackPressedDispatcher: OnBackPressedDispatcher,
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
val content: @Composable (modal: QMUIModal) -> Unit
) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskTouchBehavior) {
@Composable
override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) {
if (visible) {
Box(
modifier = Modifier
.fillMaxSize()
.background(mask)
.let {
if (maskTouchBehavior == MaskTouchBehavior.penetrate) {
it
} else {
it.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
enabled = maskTouchBehavior == MaskTouchBehavior.dismiss
) {
dismiss()
}
}
}
)
content(this)
} else {
DisposableEffect("") {
onDispose {
dismissFinishAction()
}
}
}
}
}
internal class AnimateModalImpl(
rootLayout: FrameLayout,
onBackPressedDispatcher: OnBackPressedDispatcher,
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
val enter: EnterTransition = fadeIn(tween(), 0f),
val exit: ExitTransition = fadeOut(tween(), 0f),
val content: @Composable AnimatedVisibilityScope.(modal: QMUIModal) -> Unit
) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskTouchBehavior) {
@Composable
override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) {
AnimatedVisibility(
visible = visible,
enter = enter,
exit = exit
) {
Box(modifier = Modifier
.fillMaxSize()
.background(mask)
.let {
if (maskTouchBehavior == MaskTouchBehavior.penetrate) {
it
} else {
it.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
enabled = maskTouchBehavior == MaskTouchBehavior.dismiss
) {
dismiss()
}
}
}
)
content(this@AnimateModalImpl)
DisposableEffect("") {
onDispose {
dismissFinishAction()
}
}
}
}
}
================================================
FILE: compose/src/main/java/com/qmuiteam/compose/modal/QMUIBottomSheet.kt
================================================
package com.qmuiteam.compose.modal
import android.util.Log
import android.view.View
import androidx.compose.animation.*
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
@Composable
fun QMUIBottomSheetList(
modal: QMUIModal,
state: LazyListState = rememberLazyListState(),
children: LazyListScope.(QMUIModal) -> Unit
) {
LazyColumn(
state = state,
modifier = Modifier.fillMaxWidth()
) {
children(modal)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityScope.QMUIBottomSheet(
modal: QMUIModal,
draggable: Boolean,
widthLimit: (maxWidth: Dp) -> Dp,
heightLimit: (maxHeight: Dp) -> Dp,
radius: Dp = 2.dp,
background: Color = Color.White,
mask: Color = DefaultMaskColor,
modifier: Modifier,
content: @Composable (QMUIModal) -> Unit
) {
BoxWithConstraints(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
val wl = widthLimit(maxWidth)
val wh = heightLimit(maxHeight)
var contentModifier = if (wl < maxWidth) {
Modifier.width(wl)
} else {
Modifier.fillMaxWidth()
}
contentModifier = contentModifier
.heightIn(max = wh.coerceAtMost(maxHeight))
if (radius > 0.dp) {
contentModifier =
contentModifier.clip(RoundedCornerShape(topStart = radius, topEnd = radius))
}
contentModifier = contentModifier
.background(background)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
}
if (draggable) {
NestScrollWrapper(modal, modifier, mask) {
Box(modifier = contentModifier) {
content(modal)
}
}
} else {
if (mask != Color.Transparent) {
Box(
modifier = Modifier
.fillMaxSize()
.animateEnterExit(
enter = fadeIn(tween()),
exit = fadeOut(tween())
)
.background(mask)
)
}
Box(modifier = modifier.then(contentModifier)) {
content(modal)
}
}
}
}
private class MutableHeight(var height: Float)
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AnimatedVisibilityScope.NestScrollWrapper(
modal: QMUIModal,
modifier: Modifier,
mask: Color,
content: @Composable () -> Unit
) {
val yOffsetState = remember {
mutableStateOf(0f)
}
val mutableContentHeight = remember {
MutableHeight(0f)
}
val contentHeight = mutableContentHeight.height
val percent = if (contentHeight <= 0f) 1f else {
((contentHeight - yOffsetState.value) / contentHeight)
.coerceAtMost(1f)
.coerceAtLeast(0f)
}
val nestedScrollConnection = remember(modal, yOffsetState) {
BottomSheetNestedScrollConnection(modal, yOffsetState, mutableContentHeight)
}
val yOffset = yOffsetState.value
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
if (mask != Color.Transparent) {
Box(
modifier = Modifier
.fillMaxSize()
.alpha(percent)
.animateEnterExit(
enter = fadeIn(tween()),
exit = fadeOut(tween())
)
.background(mask)
)
Box(modifier = modifier
.graphicsLayer { translationY = yOffset }
.nestedScroll(nestedScrollConnection)
.onGloballyPositioned {
mutableContentHeight.height = it.size.height.toFloat()
}
) {
content()
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
fun View.qmuiBottomSheet(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
draggable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
enter: EnterTransition = slideInVertically(tween()) { it },
exit: ExitTransition = slideOutVertically(tween()) { it },
widthLimit: (maxWidth: Dp) -> Dp = { it.coerceAtMost(420.dp) },
heightLimit: (maxHeight: Dp) -> Dp = { if (it < 640.dp) it - 40.dp else it * 0.85f },
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
return qmuiModal(
Color.Transparent,
systemCancellable,
maskTouchBehavior,
modalHostProvider = modalHostProvider,
enter = EnterTransition.None,
exit = ExitTransition.None,
) { modal ->
QMUIBottomSheet(
modal,
draggable,
widthLimit,
heightLimit,
radius,
background,
mask,
Modifier.animateEnterExit(
enter = enter,
exit = exit
),
content
)
}
}
private class BottomSheetNestedScrollConnection(
val modal: QMUIModal,
val yOffsetStateFlow: MutableState,
val contentHeight: MutableHeight
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if(source == NestedScrollSource.Fling){
return Offset.Zero
}
val currentOffset = yOffsetStateFlow.value
if(available.y < 0 && currentOffset > 0){
val consume = available.y.coerceAtLeast(-currentOffset)
yOffsetStateFlow.value = currentOffset + consume
return Offset(0f, consume)
}
return super.onPreScroll(available, source)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if(source == NestedScrollSource.Fling){
return Offset.Zero
}
if (available.y > 0) {
yOffsetStateFlow.value = yOffsetStateFlow.value + available.y
return Offset(0f, available.y)
}
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (yOffsetStateFlow.value > 0) {
if (available.y > 0 || (available.y == 0f && yOffsetStateFlow.value > contentHeight.height / 2)) {
modal.dismiss()
} else {
val animated = Animatable(yOffsetStateFlow.value, Float.VectorConverter)
animated.asState()
animated.animateTo(0f, tween()){
yOffsetStateFlow.value = value
}
}
return available
}
return Velocity.Zero
}
}
================================================
FILE: compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt
================================================
package com.qmuiteam.compose.modal
import android.view.View
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.Indication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.qmuiteam.compose.R
import com.qmuiteam.compose.core.ui.*
val DefaultDialogPaddingHor = 20.dp
@Composable
fun QMUIDialog(
modal: QMUIModal,
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiDialogVerEdgeProtectionMargin,
widthLimit: Dp = 360.dp,
radius: Dp = 2.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
) {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horEdge, vertical = verEdge),
contentAlignment = Alignment.Center
) {
var modifier = if (widthLimit < maxWidth) {
Modifier.width(widthLimit)
} else {
Modifier.fillMaxWidth()
}
if (radius > 0.dp) {
modifier = modifier.clip(RoundedCornerShape(radius))
}
modifier = modifier
.background(background)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { }
Box(modifier = modifier) {
content(modal)
}
}
}
@Composable
fun QMUIDialogActions(
modal: QMUIModal,
actions: List
){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 6.dp, end = 6.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.End
) {
actions.forEach {
QMUIDialogAction(
text = it.text,
enabled = it.enabled,
color = it.color
) {
it.onClick(modal)
}
}
}
}
@Composable
fun QMUIDialogMsg(
modal: QMUIModal,
title: String,
content: String,
actions: List
) {
Column {
QMUIDialogTitle(title)
QMUIDialogMsgContent(content)
QMUIDialogActions(modal, actions)
}
}
@Composable
fun QMUIDialogList(
modal: QMUIModal,
maxHeight: Dp = Dp.Unspecified,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(vertical = 8.dp),
children: LazyListScope.(QMUIModal) -> Unit
) {
LazyColumn(
state = state,
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, maxHeight),
contentPadding = contentPadding
) {
children(modal)
}
}
@Composable
fun QMUIDialogMarkList(
modal: QMUIModal,
list: List,
markIndex: Int,
state: LazyListState = rememberLazyListState(markIndex),
maxHeight: Dp = Dp.Unspecified,
itemIndication: Indication = rememberRipple(color = qmuiIndicationColor),
itemTextSize: TextUnit = 17.sp,
itemTextColor: Color = qmuiTextMainColor,
itemTextFontWeight: FontWeight = FontWeight.Medium,
itemTextFontFamily: FontFamily? = null,
itemMarkTintColor: Color = qmuiPrimaryColor,
contentPadding: PaddingValues = PaddingValues(vertical = 8.dp),
onItemClick: (modal: QMUIModal, index: Int) -> Unit
) {
QMUIDialogList(modal, maxHeight, state, contentPadding) {
itemsIndexed(list) { index, item ->
QMUIItem(
title = item,
indication = itemIndication,
titleOnlyFontSize = itemTextSize,
titleColor = itemTextColor,
titleFontSize = itemTextSize,
titleFontWeight = itemTextFontWeight,
titleFontFamily = itemTextFontFamily,
accessory = {
if (markIndex == index) {
Image(
painter = painterResource(id = R.drawable.ic_qmui_mark),
contentDescription = "",
colorFilter = ColorFilter.tint(itemMarkTintColor)
)
}
}
) {
onItemClick(modal, index)
}
}
}
}
@Composable
fun QMUIDialogMutiCheckList(
modal: QMUIModal,
list: List,
checked: Set,
disabled: Set = emptySet(),
disableAlpha: Float = 0.5f,
state: LazyListState = rememberLazyListState(0),
maxHeight: Dp = Dp.Unspecified,
itemIndication: Indication = rememberRipple(color = qmuiIndicationColor),
itemTextSize: TextUnit = 17.sp,
itemTextColor: Color = qmuiTextMainColor,
itemTextFontWeight: FontWeight = FontWeight.Medium,
itemTextFontFamily: FontFamily? = null,
itemCheckNormalTint: Color = qmuiSeparatorColor,
itemCheckCheckedTint: Color = qmuiPrimaryColor,
contentPadding: PaddingValues = PaddingValues(vertical = 8.dp),
onItemClick: (modal: QMUIModal, index: Int) -> Unit
) {
QMUIDialogList(modal, maxHeight, state, contentPadding) {
itemsIndexed(list) { index, item ->
val isDisabled = disabled.contains(index)
val onClick: (() -> Unit)? = if(isDisabled) null else {
{
onItemClick(modal, index)
}
}
QMUIItem(
title = item,
indication = itemIndication,
titleOnlyFontSize = itemTextSize,
titleColor = itemTextColor,
titleFontSize = itemTextSize,
titleFontWeight = itemTextFontWeight,
titleFontFamily = itemTextFontFamily,
alpha = if(isDisabled) disableAlpha else 1f,
accessory = {
if (checked.contains(index)) {
Image(
painter = painterResource(id = R.drawable.ic_qmui_checkbox_checked),
contentDescription = "",
colorFilter = ColorFilter.tint(itemCheckCheckedTint)
)
} else {
Image(
painter = painterResource(id = R.drawable.ic_qmui_checkbox_normal),
contentDescription = "",
colorFilter = ColorFilter.tint(itemCheckNormalTint)
)
}
},
onClick = onClick
)
}
}
}
@Composable
fun QMUIDialogTitle(
text: String,
fontSize: TextUnit = 16.sp,
textAlign: TextAlign? = null,
color: Color = Color.Black,
fontWeight: FontWeight? = FontWeight.Bold,
fontFamily: FontFamily? = null,
maxLines: Int = Int.MAX_VALUE,
lineHeight: TextUnit = 20.sp,
) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.padding(
top = 24.dp,
start = DefaultDialogPaddingHor,
end = DefaultDialogPaddingHor,
),
textAlign = textAlign,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = maxLines,
lineHeight = lineHeight
)
}
@Composable
fun QMUIDialogMsgContent(
text: String,
fontSize: TextUnit = 14.sp,
textAlign: TextAlign? = null,
color: Color = Color.Black,
fontWeight: FontWeight? = FontWeight.Normal,
fontFamily: FontFamily? = null,
maxLines: Int = Int.MAX_VALUE,
lineHeight: TextUnit = 16.sp,
) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.padding(
start = DefaultDialogPaddingHor,
end = DefaultDialogPaddingHor,
top = 16.dp,
bottom = 24.dp
),
textAlign = textAlign,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = maxLines,
lineHeight = lineHeight
)
}
@Composable
fun QMUIDialogAction(
text: String,
fontSize: TextUnit = 14.sp,
color: Color = qmuiPrimaryColor,
fontWeight: FontWeight? = FontWeight.Bold,
fontFamily: FontFamily? = null,
paddingVer: Dp = 9.dp,
paddingHor: Dp = 14.dp,
enabled: Boolean = true,
onClick: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
Text(
text = text,
modifier = Modifier
.padding(horizontal = paddingHor, vertical = paddingVer)
.alpha(if (isPressed.value) 0.5f else 1f)
.clickable(
enabled = enabled,
interactionSource = interactionSource,
indication = null
) {
onClick.invoke()
},
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily
)
}
fun View.qmuiDialog(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
enter: EnterTransition = fadeIn(tween(), 0f),
exit: ExitTransition = fadeOut(tween(), 0f),
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiDialogVerEdgeProtectionMargin,
widthLimit: Dp = 360.dp,
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
return qmuiModal(
mask,
systemCancellable,
maskTouchBehavior,
modalHostProvider = modalHostProvider,
enter = enter,
exit = exit
) { modal ->
QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content)
}
}
fun View.qmuiStillDialog(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
horEdge: Dp = 20.dp,
verEdge: Dp = 20.dp,
widthLimit: Dp = 360.dp,
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
return qmuiStillModal(mask, systemCancellable, maskTouchBehavior, modalHostProvider = modalHostProvider) { modal ->
QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content)
}
}
================================================
FILE: compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt
================================================
package com.qmuiteam.compose.modal
import android.os.SystemClock
import android.view.View
import android.view.Window
import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import com.qmuiteam.compose.R
import com.qmuiteam.compose.core.ui.qmuiPrimaryColor
val DefaultMaskColor = Color.Black.copy(alpha = 0.5f)
enum class MaskTouchBehavior{
dismiss, penetrate, none
}
private class ModalHolder(var current: QMUIModal? = null)
class QMUIModalAction(
val text: String,
val enabled: Boolean = true,
val color: Color = qmuiPrimaryColor,
val onClick: (QMUIModal) -> Unit
)
private class ShowingModals {
val modals = mutableMapOf()
}
@Composable
fun QMUIModal(
isVisible: Boolean,
mask: Color = DefaultMaskColor,
enter: EnterTransition = fadeIn(tween(), 0f),
exit: ExitTransition = fadeOut(tween(), 0f),
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
doOnShow: QMUIModal.Action? = null,
doOnDismiss: QMUIModal.Action? = null,
uniqueId: Long = SystemClock.elapsedRealtimeNanos(),
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit
) {
val modalHolder = remember {
ModalHolder(null)
}
if (isVisible) {
if (modalHolder.current == null) {
val modal = LocalView.current.qmuiModal(
mask,
systemCancellable,
maskTouchBehavior,
uniqueId,
modalHostProvider,
enter,
exit,
content
)
doOnShow?.let { modal.doOnShow(it) }
doOnDismiss?.let { modal.doOnDismiss(it) }
modalHolder.current = modal
}
} else {
modalHolder.current?.dismiss()
}
DisposableEffect("") {
object : DisposableEffectResult {
override fun dispose() {
modalHolder.current?.dismiss()
}
}
}
}
interface QMUIModal {
fun show(): QMUIModal
fun dismiss()
fun isShowing(): Boolean
fun doOnShow(listener: Action): QMUIModal
fun doOnDismiss(listener: Action): QMUIModal
fun removeOnShowAction(listener: Action): QMUIModal
fun removeOnDismissAction(listener: Action): QMUIModal
fun interface Action {
fun invoke(modal: QMUIModal)
}
}
fun interface ModalHostProvider {
fun provide(view: View): Pair
}
class ActivityHostModalProvider : ModalHostProvider {
override fun provide(view: View): Pair {
val contentLayout =
view.rootView.findViewById(Window.ID_ANDROID_CONTENT) ?: throw RuntimeException("View is not attached to Activity")
val activity = contentLayout.context as? AppCompatActivity ?: throw RuntimeException("view's rootView's context is not AppCompatActivity")
return contentLayout to activity.onBackPressedDispatcher
}
}
val DefaultModalHostProvider = ActivityHostModalProvider()
fun View.qmuiModal(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
uniqueId: Long = SystemClock.elapsedRealtimeNanos(),
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
enter: EnterTransition = fadeIn(tween(), 0f),
exit: ExitTransition = fadeOut(tween(), 0f),
content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit
): QMUIModal {
if (!isAttachedToWindow) {
throw RuntimeException("View is not attached to window")
}
val modalHost = modalHostProvider.provide(this)
val modal = AnimateModalImpl(
modalHost.first,
modalHost.second,
mask,
systemCancellable,
maskTouchBehavior,
enter,
exit,
content
)
val hostView = modalHost.first
handleModelUnique(hostView, modal, uniqueId)
return modal
}
fun View.qmuiStillModal(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss,
uniqueId: Long = SystemClock.elapsedRealtimeNanos(),
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
if (!isAttachedToWindow) {
throw RuntimeException("View is not attached to window")
}
val modalHost = modalHostProvider.provide(this)
val modal = StillModalImpl(modalHost.first, modalHost.second, mask, systemCancellable, maskTouchBehavior, content)
val hostView = modalHost.first
handleModelUnique(hostView, modal, uniqueId)
return modal
}
private fun handleModelUnique(hostView: FrameLayout, modal: QMUIModal, uniqueId: Long) {
val showingModals = (hostView.getTag(R.id.qmui_modals) as? ShowingModals) ?: ShowingModals().also {
hostView.setTag(R.id.qmui_modals, it)
}
modal.doOnShow {
showingModals.modals.put(uniqueId, it)?.dismiss()
}
modal.doOnDismiss {
if (showingModals.modals[uniqueId] == it) {
showingModals.modals.remove(uniqueId)
}
}
}
================================================
FILE: compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt
================================================
package com.qmuiteam.compose.modal
import android.view.View
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.qmuiteam.compose.core.ui.qmuiCommonHorSpace
import com.qmuiteam.compose.core.ui.qmuiToastVerEdgeProtectionMargin
import kotlinx.coroutines.*
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@Composable
fun QMUIToast(
modal: QMUIModal,
radius: Dp = 8.dp,
background: Color = Color.DarkGray,
content: @Composable BoxScope.(QMUIModal) -> Unit
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(radius))
.background(background)
) {
content(modal)
}
}
fun View.qmuiToast(
text: String,
textColor: Color = Color.White,
fontSize: TextUnit = 16.sp,
duration: Long = 1000,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
alignment: Alignment = Alignment.BottomCenter,
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiToastVerEdgeProtectionMargin,
radius: Dp = 8.dp,
background: Color = Color.Black,
enter: EnterTransition = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
): QMUIModal {
return qmuiToast(
duration,
modalHostProvider,
alignment,
horEdge,
verEdge,
radius,
background,
enter,
exit
) {
Text(
text = text,
color = textColor,
fontSize = fontSize,
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 16.dp)
.align(Alignment.Center)
)
}
}
@OptIn(ExperimentalAnimationApi::class)
fun View.qmuiToast(
duration: Long = 1000,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
alignment: Alignment = Alignment.BottomCenter,
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiToastVerEdgeProtectionMargin,
radius: Dp = 8.dp,
background: Color = Color.Black,
enter: EnterTransition = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
content: @Composable BoxScope.(QMUIModal) -> Unit
): QMUIModal {
var job: Job? = null
return qmuiModal(
Color.Transparent,
false,
MaskTouchBehavior.penetrate,
-1,
modalHostProvider,
enter = EnterTransition.None,
exit = ExitTransition.None
) { modal ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horEdge, vertical = verEdge),
contentAlignment = alignment
) {
Box(
modifier = Modifier
.animateEnterExit(
enter = enter,
exit = exit
)
) {
QMUIToast(modal, radius, background, content)
}
}
}.doOnShow {
job = scope.launch {
delay(duration)
job = null
it.dismiss()
}
}.doOnDismiss {
job?.cancel()
job = null
}.show()
}
fun View.qmuiStillToast(
text: String,
textColor: Color = Color.White,
fontSize: TextUnit = 16.sp,
duration: Long = 1000,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
alignment: Alignment = Alignment.BottomCenter,
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiToastVerEdgeProtectionMargin,
radius: Dp = 8.dp,
background: Color = Color.Black
): QMUIModal {
return qmuiStillToast(
duration,
modalHostProvider,
alignment,
horEdge,
verEdge,
radius,
background
) {
Text(
text = text,
color = textColor,
fontSize = fontSize,
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 16.dp)
.align(Alignment.Center)
)
}
}
@OptIn(ExperimentalAnimationApi::class)
fun View.qmuiStillToast(
duration: Long = 1000,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
alignment: Alignment = Alignment.BottomCenter,
horEdge: Dp = qmuiCommonHorSpace,
verEdge: Dp = qmuiToastVerEdgeProtectionMargin,
radius: Dp = 8.dp,
background: Color = Color.Black,
content: @Composable BoxScope.(QMUIModal) -> Unit
): QMUIModal {
var job: Job? = null
return qmuiStillModal(
Color.Transparent,
false,
MaskTouchBehavior.penetrate,
-1,
modalHostProvider,
) { modal ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horEdge, vertical = verEdge),
contentAlignment = alignment
) {
QMUIToast(modal, radius, background, content)
}
}.doOnShow {
job = scope.launch {
delay(duration)
job = null
it.dismiss()
}
}.doOnDismiss {
job?.cancel()
job = null
}.show()
}
================================================
FILE: compose/src/main/res/values/ids.xml
================================================
================================================
FILE: compose/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt
================================================
package com.qmuiteam.compose
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
}
================================================
FILE: compose-core/.gitignore
================================================
/build
================================================
FILE: compose-core/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.composeCoreVer
android {
compileSdk = Dep.compileSdk
buildFeatures {
compose = true
}
defaultConfig {
minSdk = Dep.minSdk
targetSdk = Dep.targetSdk
}
buildTypes {
getByName("release"){
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
kotlinOptions {
jvmTarget = Dep.kotlinJvmTarget
}
composeOptions {
kotlinCompilerExtensionVersion = Dep.Compose.version
}
}
dependencies {
api(Dep.AndroidX.appcompat)
api(Dep.Compose.ui)
api(Dep.Compose.animation)
api(Dep.Compose.material)
api(Dep.Compose.compiler)
}
================================================
FILE: compose-core/proguard-rules.pro
================================================
# 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
================================================
FILE: compose-core/src/androidTest/java/com/qmuiteam/compose/ExampleInstrumentedTest.kt
================================================
package com.qmuiteam.compose
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.qmuiteam.compose", appContext.packageName)
}
}
================================================
FILE: compose-core/src/main/AndroidManifest.xml
================================================
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ex/DrawScopeEx.kt
================================================
package com.qmuiteam.compose.core.ex
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.qmuiteam.compose.core.ui.qmuiSeparatorColor
fun DrawScope.drawTopSeparator(color: Color = qmuiSeparatorColor, insetStart: Dp = 0.dp, insetEnd: Dp = 0.dp) {
drawLine(
color = color,
start = Offset(insetStart.toPx(), 0f),
end = Offset(size.width - insetEnd.toPx(), 0f),
cap = StrokeCap.Square
)
}
fun DrawScope.drawBottomSeparator(color: Color = qmuiSeparatorColor, insetStart: Dp = 0.dp, insetEnd: Dp = 0.dp) {
drawLine(
color = color,
start = Offset(insetStart.toPx(), size.height),
end = Offset(size.width - insetEnd.toPx(), size.height),
cap = StrokeCap.Square
)
}
fun DrawScope.drawLeftSeparator(color: Color = qmuiSeparatorColor, insetStart: Dp = 0.dp, insetEnd: Dp = 0.dp) {
drawLine(
color = color,
start = Offset(0f, insetStart.toPx()),
end = Offset(0f, size.height - insetEnd.toPx()),
cap = StrokeCap.Square
)
}
fun DrawScope.drawRightSeparator(color: Color = qmuiSeparatorColor, insetStart: Dp = 0.dp, insetEnd: Dp = 0.dp) {
drawLine(
color = color,
start = Offset(size.width, insetStart.toPx()),
end = Offset(size.width, size.height - insetEnd.toPx()),
cap = StrokeCap.Square
)
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/helper/Dimen.kt
================================================
package com.qmuiteam.compose.core.helper
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun OnePx(): Dp {
return (1 / LocalDensity.current.density).dp
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/helper/Global.kt
================================================
package com.qmuiteam.compose.core.helper
object QMUIGlobal {
var debug: Boolean = false
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/helper/Log.kt
================================================
package com.qmuiteam.compose.core.helper
import android.util.Log
interface QMUILogDelegate {
fun e(tag: String, msg: String, throwable: Throwable? = null)
fun w(tag: String, msg: String, throwable: Throwable? = null)
fun i(tag: String, msg: String, throwable: Throwable? = null)
fun d(tag: String, msg: String, throwable: Throwable? = null)
}
object SystemLogDelegate : QMUILogDelegate {
override fun e(tag: String, msg: String, throwable: Throwable?) {
Log.e(tag, msg, throwable)
}
override fun w(tag: String, msg: String, throwable: Throwable?) {
Log.w(tag, msg, throwable)
}
override fun i(tag: String, msg: String, throwable: Throwable?) {
Log.i(tag, msg, throwable)
}
override fun d(tag: String, msg: String, throwable: Throwable?) {
Log.d(tag, msg, throwable)
}
}
object QMUILog {
var delegate: QMUILogDelegate? = SystemLogDelegate
fun e(tag: String, msg: String, throwable: Throwable? = null) {
delegate?.e(tag, msg, throwable)
}
fun w(tag: String, msg: String, throwable: Throwable? = null) {
delegate?.w(tag, msg, throwable)
}
fun i(tag: String, msg: String, throwable: Throwable? = null) {
delegate?.i(tag, msg, throwable)
}
fun d(tag: String, msg: String, throwable: Throwable? = null) {
delegate?.d(tag, msg, throwable)
}
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/helper/LogTag.kt
================================================
package com.qmuiteam.compose.core.helper
interface LogTag {
val TAG: String
get() = getTag(javaClass)
}
fun logTag(clazz: Class<*>): LogTag = object : LogTag {
override val TAG = getTag(clazz)
}
inline fun logTag(): LogTag = logTag(T::class.java)
private fun getTag(clazz: Class<*>): String {
val tag = clazz.simpleName
return if (tag.length <= 23) {
tag
} else {
tag.substring(0, 23)
}
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/provider/WindowInsets.kt
================================================
package com.qmuiteam.compose.core.provider
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.qmuiteam.compose.core.R
val QMUILocalWindowInsets = staticCompositionLocalOf { WindowInsetsCompat.CONSUMED }
@Composable
fun QMUIWindowInsetsProvider(content: @Composable () -> Unit) {
val view = LocalView.current
val windowInsets = remember(view) {
mutableStateOf(view.getTag(R.id.qmui_window_inset_cache) as? WindowInsetsCompat ?: WindowInsetsCompat.CONSUMED)
}
LaunchedEffect(view) {
ViewCompat.setOnApplyWindowInsetsListener(view, OnApplyWindowInsetsListener { _, insets ->
windowInsets.value = insets
view.setTag(R.id.qmui_window_inset_cache, insets)
return@OnApplyWindowInsetsListener insets
})
view.requestApplyInsets()
}
CompositionLocalProvider(QMUILocalWindowInsets provides windowInsets.value) {
content()
}
}
data class DpInsets(val left: Dp, val top: Dp, val right: Dp, val bottom: Dp) {
companion object {
val NONE = DpInsets(0.dp, 0.dp, 0.dp, 0.dp)
}
}
@Composable
fun Insets.dp(): DpInsets {
if (this == Insets.NONE) {
return DpInsets.NONE
}
return with(LocalDensity.current) {
DpInsets(
(left / density).dp,
(top / density).dp,
(right / density).dp,
(bottom / density).dp
)
}
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt
================================================
package com.qmuiteam.compose.core.ui
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
val qmuiPrimaryColor = Color(0xFF00A8E1)
val qmuiSeparatorColor = Color(0xFFCCCCCC)
val qmuiIndicationColor = Color(0xFF777777)
val qmuiTextMainColor = Color.Black
val qmuiTextDescColor = Color(0xFF666666)
val qmuiTopBarHeight = 48.dp
val qmuiTopBarZIndex = 32f
val qmuiCommonHorSpace = 20.dp
val qmuiScrollAlphaChangeMaxOffset = 20.dp
val qmuiDialogVerEdgeProtectionMargin = 44.dp
val qmuiToastVerEdgeProtectionMargin = 96.dp
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ui/PressWithAlphaBox.kt
================================================
package com.qmuiteam.compose.core.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@Composable
fun PressWithAlphaBox(
modifier: Modifier = Modifier,
enable: Boolean = true,
pressAlpha: Float = 0.5f,
disableAlpha: Float = 0.5f,
onClick: (() -> Unit)? = null,
content: @Composable BoxScope.() -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
Box(modifier = Modifier
.alpha(if (!enable) disableAlpha else if (isPressed.value) pressAlpha else 1f)
.clickable(enabled = enable, interactionSource = interactionSource, indication = null) {
onClick?.invoke()
}
.then(modifier),
content = content
)
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIIcon.kt
================================================
package com.qmuiteam.compose.core.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.qmuiteam.compose.core.R
@Composable
fun QMUIChevronIcon(tint: Color? = null) {
Image(
painter = painterResource(id = R.drawable.ic_qmui_chevron),
contentDescription = "",
colorFilter = tint?.let { ColorFilter.tint(it) }
)
}
enum class CheckStatus {
none, partial, checked
}
@Composable
fun QMUICheckBox(
size: Dp,
status: CheckStatus = CheckStatus.none,
isEnabled: Boolean = true,
tint: Color?,
background: Color = Color.Transparent
) {
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
) {
AnimatedVisibility(
visible = status == CheckStatus.none,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_normal, isEnabled, tint, background)
}
AnimatedVisibility(
visible = status == CheckStatus.checked,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_checked, isEnabled, tint, background)
}
AnimatedVisibility(
visible = status == CheckStatus.partial,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_partial, isEnabled, tint, background)
}
}
}
@Composable
private fun QMUICheckBoxImage(
resourceId: Int,
isEnabled: Boolean = true,
tint: Color?,
background: Color = Color.Transparent
){
Image(
painter = painterResource(id = resourceId),
contentScale = ContentScale.Fit,
contentDescription = "",
colorFilter = tint?.let { ColorFilter.tint(it) },
modifier = Modifier
.fillMaxSize()
.let {
if (isEnabled) {
it
} else {
it.alpha(0.5f)
}
}.let {
if (background != Color.Transparent) {
it.background(background)
} else {
it
}
}
)
}
@Composable
fun QMUIMarkIcon(
modifier: Modifier = Modifier,
tint: Color? = null
) {
Image(
painter = painterResource(id = R.drawable.ic_qmui_mark),
contentDescription = "",
colorFilter = tint?.let { ColorFilter.tint(it) },
modifier = modifier
)
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUIItem.kt
================================================
package com.qmuiteam.compose.core.ui
import androidx.compose.foundation.Indication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun QMUIItem(
title: String,
detail: String = "",
alpha: Float = 1f,
background: Color = Color.Transparent,
indication: Indication = rememberRipple(color = qmuiIndicationColor),
titleFontSize: TextUnit = 16.sp,
titleOnlyFontSize: TextUnit = 17.sp,
titleColor: Color = qmuiTextMainColor,
titleFontWeight: FontWeight = FontWeight.Medium,
titleFontFamily: FontFamily? = null,
titleLineHeight: TextUnit = 20.sp,
detailFontSize: TextUnit = 12.sp,
detailColor: Color = qmuiTextDescColor,
detailFontWeight: FontWeight = FontWeight.Normal,
detailFontFamily: FontFamily? = null,
detailLineHeight: TextUnit = 17.sp,
minHeight: Dp = 56.dp,
paddingHor: Dp = qmuiCommonHorSpace,
paddingVer: Dp = 12.dp,
gapBetweenTitleAndDetail: Dp = 4.dp,
accessory: @Composable (RowScope.() -> Unit)? = null,
drawBehind: (DrawScope.() -> Unit)? = null,
onClick: (() -> Unit)? = null
) {
Row(modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = minHeight)
.alpha(alpha)
.background(background)
.drawBehind {
drawBehind?.invoke(this)
}
.clickable(
enabled = onClick != null,
interactionSource = remember { MutableInteractionSource() },
indication = indication
) {
onClick?.invoke()
}
.padding(horizontal = paddingHor, vertical = paddingVer),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
color = titleColor,
modifier = Modifier.fillMaxWidth(),
fontSize = if (detail.isNotBlank()) titleFontSize else titleOnlyFontSize,
fontWeight = titleFontWeight,
fontFamily = titleFontFamily,
lineHeight = titleLineHeight
)
if (detail.isNotBlank()) {
Text(
text = detail,
color = detailColor,
modifier = Modifier
.fillMaxWidth()
.padding(top = gapBetweenTitleAndDetail),
fontSize = detailFontSize,
fontWeight = detailFontWeight,
fontFamily = detailFontFamily,
lineHeight = detailLineHeight
)
}
}
accessory?.invoke(this)
}
}
================================================
FILE: compose-core/src/main/java/com/qmuiteam/compose/core/ui/QMUITopBar.kt
================================================
package com.qmuiteam.compose.core.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.zIndex
import androidx.core.view.WindowInsetsCompat
import com.qmuiteam.compose.core.R
import com.qmuiteam.compose.core.helper.OnePx
import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets
import com.qmuiteam.compose.core.provider.dp
fun interface QMUITopBarItem {
@Composable
fun Compose(topBarHeight: Dp)
}
interface QMUITopBarTitleLayout {
@Composable
fun Compose(title: CharSequence, subTitle: CharSequence, alignTitleCenter: Boolean)
}
class DefaultQMUITopBarTitleLayout(
val titleColor: Color = Color.White,
val titleFontWeight: FontWeight = FontWeight.Bold,
val titleFontFamily: FontFamily? = null,
val titleFontSize: TextUnit = 16.sp,
val titleOnlyFontSize: TextUnit = 17.sp,
val subTitleColor: Color = Color.White.copy(alpha = 0.8f),
val subTitleFontWeight: FontWeight = FontWeight.Normal,
val subTitleFontFamily: FontFamily? = null,
val subTitleFontSize: TextUnit = 11.sp
) : QMUITopBarTitleLayout {
@Composable
override fun Compose(title: CharSequence, subTitle: CharSequence, alignTitleCenter: Boolean) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = if (alignTitleCenter) Alignment.CenterHorizontally else Alignment.Start
) {
Text(
title.toString(),
color = titleColor,
fontWeight = titleFontWeight,
fontFamily = titleFontFamily,
fontSize = if (subTitle.isNotEmpty()) titleFontSize else titleOnlyFontSize,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
if (subTitle.isNotEmpty()) {
Text(
subTitle.toString(),
color = subTitleColor,
fontWeight = subTitleFontWeight,
fontFamily = subTitleFontFamily,
fontSize = subTitleFontSize,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
}
}
open class QMUITopBarBackIconItem(
tint: Color = Color.White,
pressAlpha: Float = 0.5f,
disableAlpha: Float = 0.5f,
enable: Boolean = true,
onClick: () -> Unit
) : QMUITopBarIconItem(
R.drawable.ic_qmui_topbar_back,
"返回",
tint,
pressAlpha,
disableAlpha,
enable,
onClick
)
open class QMUITopBarIconItem(
@DrawableRes val icon: Int,
val contentDescription: String = "",
val tint: Color = Color.White,
val pressAlpha: Float = 0.5f,
val disableAlpha: Float = 0.5f,
val enable: Boolean = true,
val onClick: () -> Unit
) : QMUITopBarItem {
@Composable
override fun Compose(topBarHeight: Dp) {
PressWithAlphaBox(
modifier = Modifier.size(topBarHeight),
enable = enable,
pressAlpha = pressAlpha,
disableAlpha = disableAlpha,
onClick = onClick
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(icon),
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(tint),
contentScale = ContentScale.Inside
)
}
}
}
open class QMUITopBarTextItem(
val text: String,
val paddingHor: Dp = 12.dp,
val fontSize: TextUnit = 14.sp,
val fontWeight: FontWeight = FontWeight.Medium,
val color: Color = Color.White,
val pressAlpha: Float = 0.5f,
val disableAlpha: Float = 0.5f,
val enable: Boolean = true,
val onClick: () -> Unit
) : QMUITopBarItem {
@Composable
override fun Compose(topBarHeight: Dp) {
PressWithAlphaBox(
modifier = Modifier
.height(topBarHeight)
.padding(horizontal = paddingHor),
enable = enable,
pressAlpha = pressAlpha,
disableAlpha = disableAlpha,
onClick = onClick
) {
Text(
text = text,
modifier = Modifier.align(Alignment.Center),
color = color,
fontSize = fontSize,
fontWeight = fontWeight
)
}
}
}
@Composable
fun QMUITopBarWithLazyScrollState(
scrollState: LazyListState,
title: CharSequence = "",
subTitle: CharSequence = "",
alignTitleCenter: Boolean = true,
height: Dp = qmuiTopBarHeight,
zIndex: Float = qmuiTopBarZIndex,
backgroundColor: Color = qmuiPrimaryColor,
changeWithBackground: Boolean = false,
scrollAlphaChangeMaxOffset: Dp = qmuiScrollAlphaChangeMaxOffset,
shadowElevation: Dp = 16.dp,
shadowAlpha: Float = 0.6f,
separatorHeight: Dp = OnePx(),
separatorColor: Color = qmuiSeparatorColor,
paddingStart: Dp = 4.dp,
paddingEnd: Dp = 4.dp,
titleBoxPaddingHor: Dp = 8.dp,
leftItems: List = emptyList(),
rightItems: List = emptyList(),
titleLayout: QMUITopBarTitleLayout = remember { DefaultQMUITopBarTitleLayout() }
){
val percent = with(LocalDensity.current){
if(scrollState.firstVisibleItemIndex > 0 || scrollState.firstVisibleItemScrollOffset.toDp() > scrollAlphaChangeMaxOffset){
1f
} else scrollState.firstVisibleItemScrollOffset.toDp() / scrollAlphaChangeMaxOffset
}
QMUITopBar(
title, subTitle,
alignTitleCenter, height, zIndex,
if(changeWithBackground) backgroundColor.copy(backgroundColor.alpha * percent) else backgroundColor,
shadowElevation, shadowAlpha * percent,
separatorHeight, separatorColor.copy(separatorColor.alpha * percent),
paddingStart, paddingEnd,
titleBoxPaddingHor, leftItems, rightItems, titleLayout
)
}
@Composable
fun QMUITopBar(
title: CharSequence,
subTitle: CharSequence = "",
alignTitleCenter: Boolean = true,
height: Dp = qmuiTopBarHeight,
zIndex: Float = qmuiTopBarZIndex,
backgroundColor: Color = qmuiPrimaryColor,
shadowElevation: Dp = 16.dp,
shadowAlpha: Float = 0.4f,
separatorHeight: Dp = OnePx(),
separatorColor: Color = qmuiSeparatorColor,
paddingStart: Dp = 4.dp,
paddingEnd: Dp = 4.dp,
titleBoxPaddingHor: Dp = 8.dp,
leftItems: List = emptyList(),
rightItems: List = emptyList(),
titleLayout: QMUITopBarTitleLayout = remember { DefaultQMUITopBarTitleLayout() }
) {
val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility(
WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout()
).dp()
Box(modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.zIndex(zIndex)
){
Box(modifier = Modifier.fillMaxSize().graphicsLayer {
this.alpha = shadowAlpha
this.shadowElevation = shadowElevation.toPx()
this.shape = RectangleShape
this.clip = shadowElevation > 0.dp
})
Box(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(top = insets.top)
.height(height)
) {
QMUITopBarContent(
title,
subTitle,
alignTitleCenter,
height,
paddingStart,
paddingEnd,
titleBoxPaddingHor,
leftItems,
rightItems,
titleLayout
)
if(separatorHeight > 0.dp && separatorColor != Color.Transparent){
Box(modifier = Modifier
.fillMaxWidth()
.height(separatorHeight)
.align(Alignment.BottomStart)
.background(separatorColor)
)
}
}
}
}
@Composable
fun QMUITopBarContent(
title: CharSequence,
subTitle: CharSequence,
alignTitleCenter: Boolean,
height: Dp = qmuiTopBarHeight,
paddingStart: Dp = 4.dp,
paddingEnd: Dp = 4.dp,
titleBoxPaddingHor: Dp = 8.dp,
leftItems: List = emptyList(),
rightItems: List = emptyList(),
titleLayout: QMUITopBarTitleLayout = remember { DefaultQMUITopBarTitleLayout() }
) {
val measurePolicy = remember(alignTitleCenter) {
MeasurePolicy { measurables, constraints ->
var centerMeasurable: Measurable? = null
var leftPlaceable: Placeable? = null
var rightPlaceable: Placeable? = null
var centerPlaceable: Placeable? = null
val usedConstraints = constraints.copy(minWidth = 0)
measurables
.forEach {
when ((it.parentData as? QMUITopBarAreaParentData)?.area ?: QMUITopBarArea.Left) {
QMUITopBarArea.Left -> {
leftPlaceable = it.measure(usedConstraints)
}
QMUITopBarArea.Right -> {
rightPlaceable = it.measure(usedConstraints)
}
QMUITopBarArea.Center -> {
centerMeasurable = it
}
}
}
val leftItemsWidth = leftPlaceable?.measuredWidth ?: 0
val rightItemsWidth = rightPlaceable?.measuredWidth ?: 0
val itemsWidthMax = maxOf(leftItemsWidth, rightItemsWidth)
val titleContainerWidth = if (alignTitleCenter) {
constraints.maxWidth - itemsWidthMax * 2
} else {
constraints.maxWidth - leftItemsWidth - rightItemsWidth
}
if (titleContainerWidth > 0) {
centerPlaceable = centerMeasurable?.measure(constraints.copy(minWidth = 0, maxWidth = titleContainerWidth))
}
layout(constraints.maxWidth, constraints.maxHeight) {
leftPlaceable?.place(0, 0, 0f)
rightPlaceable?.let {
it.place(constraints.maxWidth - it.measuredWidth, 0, 1f)
}
centerPlaceable?.let {
if (alignTitleCenter) {
it.place(itemsWidthMax, 0, 2f)
} else {
it.place(leftItemsWidth, 0, 2f)
}
}
}
}
}
Layout(
content = {
Row(
modifier = Modifier
.fillMaxHeight()
.qmuiTopBarArea(QMUITopBarArea.Left),
verticalAlignment = Alignment.CenterVertically
) {
leftItems.forEach {
it.Compose(height)
}
}
Box(
modifier = Modifier
.fillMaxHeight()
.qmuiTopBarArea(QMUITopBarArea.Center)
.padding(horizontal = titleBoxPaddingHor),
contentAlignment = Alignment.CenterStart
) {
titleLayout.Compose(title, subTitle, alignTitleCenter)
}
Row(
modifier = Modifier
.fillMaxHeight()
.qmuiTopBarArea(QMUITopBarArea.Right),
verticalAlignment = Alignment.CenterVertically
) {
rightItems.forEach {
it.Compose(height)
}
}
},
measurePolicy = measurePolicy,
modifier = Modifier
.fillMaxWidth()
.height(height)
.padding(start = paddingStart, end = paddingEnd)
)
}
internal enum class QMUITopBarArea { Left, Center, Right }
internal data class QMUITopBarAreaParentData(
var area: QMUITopBarArea = QMUITopBarArea.Left
)
internal fun Modifier.qmuiTopBarArea(area: QMUITopBarArea) = this.then(
QMUITopBarAreaModifier(
area = area,
inspectorInfo = debugInspectorInfo {
name = "area"
value = area.name
}
)
)
internal class QMUITopBarAreaModifier(
val area: QMUITopBarArea,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): QMUITopBarAreaParentData {
return ((parentData as? QMUITopBarAreaParentData) ?: QMUITopBarAreaParentData()).also {
it.area = area
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? QMUITopBarAreaParentData ?: return false
return area == otherModifier.area
}
override fun hashCode(): Int {
return area.hashCode()
}
override fun toString(): String =
"QMUITopBarAreaModifier(area=$area)"
}
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_checkbox_checked.xml
================================================
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_checkbox_normal.xml
================================================
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_checkbox_partial.xml
================================================
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_chevron.xml
================================================
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_mark.xml
================================================
================================================
FILE: compose-core/src/main/res/drawable/ic_qmui_topbar_back.xml
================================================
================================================
FILE: compose-core/src/main/res/values/qmui_ids.xml
================================================
================================================
FILE: compose-core/src/test/java/com/qmuiteam/compose/ExampleUnitTest.kt
================================================
package com.qmuiteam.compose
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
}
================================================
FILE: deploy.sh
================================================
#!/usr/bin/env bash
#./deploy.sh qmui publishToMavenLocal
#./deploy.sh arch publishToMavenLocal
#./deploy.sh type publishToMavenLocal
#./deploy.sh compose-core publishToMavenLocal
#./deploy.sh compose publishToMavenLocal
#./deploy.sh photo publishToMavenLocal
#./deploy.sh qmui publish
#./deploy.sh arch publish
#./deploy.sh type publish
#./deploy.sh compose-core publish
#./deploy.sh compose publish
#./deploy.sh photo publish
if [[ "qmui" == "$1" ]]
then
buildCmd="./gradlew :qmui:clean :qmui:build qmui:$2"
$buildCmd
elif [[ "arch" == "$1" ]]
then
buildCmd="./gradlew :arch:clean :arch:build :arch:$2"
$buildCmd
buildCmd="./gradlew :arch-annotation:clean :arch-annotation:build :arch-annotation:$2"
$buildCmd
buildCmd="./gradlew :arch-compiler:clean :arch-compiler:build :arch-compiler:$2"
$buildCmd
elif [[ "type" == "$1" ]]
then
buildCmd="./gradlew :type:clean :type:build :type:$2"
$buildCmd
elif [[ "compose-core" == "$1" ]]
then
buildCmd="./gradlew :compose-core:clean :compose-core:build :compose-core:$2"
$buildCmd
elif [[ "compose" == "$1" ]]
then
buildCmd="./gradlew :compose:clean :compose:build :compose:$2"
$buildCmd
elif [[ "photo" == "$1" ]]
then
buildCmd="./gradlew :photo:clean :photo:build :photo:$2"
$buildCmd
buildCmd="./gradlew :photo-coil:clean :photo-coil:build :photo-coil:$2"
$buildCmd
buildCmd="./gradlew :photo-glide:clean :photo-glide:build :photo-glide:$2"
$buildCmd
fi
================================================
FILE: editor/.gitignore
================================================
/build
================================================
FILE: editor/build.gradle.kts
================================================
import com.qmuiteam.plugin.Dep
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
`maven-publish`
signing
id("qmui-publish")
}
version = Dep.QMUI.editorVer
android {
compileSdk = Dep.compileSdk
defaultConfig {
minSdk = Dep.minSdk
targetSdk = Dep.targetSdk
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Dep.Compose.version
}
buildTypes {
getByName("release"){
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
kotlinOptions {
jvmTarget = Dep.kotlinJvmTarget
}
}
dependencies {
implementation(project(":compose-core"))
}
================================================
FILE: editor/consumer-rules.pro
================================================
================================================
FILE: editor/proguard-rules.pro
================================================
# 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
================================================
FILE: editor/src/main/AndroidManifest.xml
================================================
================================================
FILE: editor/src/main/java/com/qmuiteam/editor/EditorBehavior.kt
================================================
package com.qmuiteam.editor
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
interface EditorBehavior {
fun apply(value: TextFieldValue): TextFieldValue
}
internal fun String.isHeaderTag(): Boolean{
return HeaderLevel.values().find { it.tag == this } != null
}
internal fun String.isBoldTag(): Boolean{
return startsWith(BoldBehavior.prefix)
}
class BoldBehavior(val weight: Int = 700) : EditorBehavior {
companion object {
val prefix = "blod"
}
val tag: String = "$prefix:$weight"
override fun apply(value: TextFieldValue): TextFieldValue {
return value.bold(this)
}
}
class StopBehavior(val target: String): EditorBehavior {
companion object {
val prefix = "stop"
}
val tag: String = "${prefix}:$target"
override fun apply(value: TextFieldValue): TextFieldValue {
return value
}
}
class TextColorBehavior(val color: Color = Color.White) : EditorBehavior {
companion object {
val prefix = "color"
}
val tag: String = "$prefix:$color"
override fun apply(value: TextFieldValue): TextFieldValue {
return value.textColor(this)
}
}
object NormalParagraphBehavior: EditorBehavior {
const val tag = "p"
override fun apply(value: TextFieldValue): TextFieldValue {
return value.quote()
}
}
object QuoteBehavior : EditorBehavior {
const val tag = "quote"
override fun apply(value: TextFieldValue): TextFieldValue {
return value.quote()
}
}
object UnOrderListBehavior : EditorBehavior {
const val tag = "ul"
override fun apply(value: TextFieldValue): TextFieldValue {
return value.unOrder()
}
}
class HeaderBehavior(val level: HeaderLevel): EditorBehavior {
override fun apply(value: TextFieldValue): TextFieldValue {
return value.header(level)
}
}
enum class HeaderLevel(val tag: String, val fontSize: TextUnit) {
h1("h1", 24.sp),
h2("h2", 22.sp),
h3("h3", 20.sp),
h4("h4", 18.sp),
h5("h5", 16.sp)
}
================================================
FILE: editor/src/main/java/com/qmuiteam/editor/QMUIEditor.kt
================================================
package com.qmuiteam.editor
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width
import com.qmuiteam.compose.core.ui.qmuiPrimaryColor
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
interface EditorDecoration {
@Composable
fun Compose()
}
class QuoteDecoration(val rect: Rect) : EditorDecoration {
@Composable
override fun Compose() {
key(this) {
val dpRect = with(LocalDensity.current) {
DpRect(rect.left.toDp(), rect.top.toDp(), rect.right.toDp(), rect.bottom.toDp())
}
Box(
Modifier
.offset(dpRect.left, dpRect.top - 6.dp)
.width(dpRect.width)
.height(dpRect.height + 12.dp)
.background(Color.LightGray)
) {
Box(
modifier = Modifier
.width(2.dp)
.fillMaxHeight()
.background(Color.Gray)
)
}
}
}
}
class UnOrderedDecoration(val rect: Rect) : EditorDecoration {
@Composable
override fun Compose() {
key(this) {
val dpRect = with(LocalDensity.current) {
DpRect(rect.left.toDp(), rect.top.toDp(), rect.right.toDp(), rect.bottom.toDp())
}
Box(
Modifier
.offset(dpRect.left, dpRect.top + dpRect.height / 2 - 2.dp)
.width(4.dp)
.height(4.dp)
.clip(CircleShape)
.background(Color.Black)
)
}
}
}
@Composable
fun QMUIEditor(
modifier: Modifier = Modifier,
value: TextFieldValue,
channel: Channel,
hint: AnnotatedString = AnnotatedString(""),
hintStyle: TextStyle = TextStyle.Default.copy(color = Color.Gray),
textStyle: TextStyle = TextStyle.Default,
focusRequester: FocusRequester = remember {
FocusRequester()
},
cursorBrush: Brush = SolidColor(qmuiPrimaryColor),
onValueChange: (TextFieldValue) -> Unit
) {
var textFieldValue by remember(value) {
mutableStateOf(value.check())
}
var editorDecorations by remember {
mutableStateOf(listOf())
}
LaunchedEffect(key1 = value) {
launch {
while (isActive) {
val behavior = channel.receive()
textFieldValue = behavior.apply(textFieldValue)
}
}
}
// TODO Fix here, BasicTextField can scroll inner , but i can't read the scroll position.
BoxWithConstraints(modifier) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
BasicTextField(
value = textFieldValue,
onTextLayout = {
val list = mutableListOf()
it.layoutInput.text.paragraphStyles.forEach { paragraph ->
val rect = if (paragraph.start == paragraph.end) {
val cursorRect = it.multiParagraph.getCursorRect(paragraph.start)
Rect(
0f,
cursorRect.top,
it.multiParagraph.width,
cursorRect.bottom
)
} else {
val start = it.multiParagraph.getBoundingBox(paragraph.start)
val end = it.multiParagraph.getBoundingBox(paragraph.end - 1)
Rect(
0f,
start.top,
it.multiParagraph.width,
end.bottom
)
}
if (paragraph.tag == QuoteBehavior.tag) {
list.add(QuoteDecoration(rect))
} else if (paragraph.tag == UnOrderListBehavior.tag) {
list.add(UnOrderedDecoration(rect))
}
}
editorDecorations = list
},
onValueChange = {
textFieldValue = updateTextFieldValue(textFieldValue, it)
onValueChange(textFieldValue)
},
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = this@BoxWithConstraints.maxHeight)
.focusRequester(focusRequester),
textStyle = textStyle,
cursorBrush = cursorBrush,
decorationBox = { innerTextField ->
Box(modifier = Modifier.fillMaxSize()) {
editorDecorations.forEach {
it.Compose()
}
}
if (textFieldValue.text.isEmpty()) {
Text(text = hint, style = hintStyle)
}
innerTextField()
}
)
}
}
}
private fun updateTextFieldValue(
current: TextFieldValue,
next: TextFieldValue
): TextFieldValue {
if (current.text == next.text) {
return TextFieldValue(current.annotatedString, next.selection, next.composition)
}
if (next.text.isBlank()) {
return TextFieldValue(AnnotatedString(""), next.selection, next.composition).check()
}
val mutableSpan = mutableListOf>()
val mutableParagraph = mutableListOf>()
current.annotatedString.spanStyles.forEach {
mutableSpan.add(MutableRange(it.item, it.start, it.end, it.tag))
}
current.annotatedString.paragraphStyles.forEach {
mutableParagraph.add(MutableRange(it.item, it.start, it.end, it.tag))
}
var indexCorrect = 0
wordEdit(current, next).list.forEach { point ->
val lastIndex = point.oldIndex + indexCorrect
if (point.action == WordEditAction.insert) {
val toInsertPos = lastIndex + 1
mutableParagraph.forEachIndexed { index, item ->
item.modifyByInsert(toInsertPos, index == mutableParagraph.size - 1)
}
val stopSpans = mutableListOf>()
val normalSpans = mutableListOf>()
mutableSpan.forEach {
if (it.tag.startsWith(StopBehavior.prefix)) {
stopSpans.add(it)
} else {
normalSpans.add(it)
}
}
mutableSpan.forEach { item ->
item.modifyByInsert(
toInsertPos,
stopSpans.find { it.end == item.end && it.tag.endsWith(item.tag) } == null
)
// update companion span.
mutableParagraph.find { it.tag == item.tag && it.start == item.start }?.let {
item.end = it.end
}
}
stopSpans.forEach {
it.modifyByInsert(toInsertPos, true)
if (it.end > it.start) {
mutableSpan.remove(it)
}
}
if (next.text[point.newIndex] == '\n') {
for (i in 0 until mutableParagraph.size) {
val paragraph = mutableParagraph[i]
if (paragraph.start <= point.newIndex && paragraph.end > point.newIndex) {
if (!paragraph.tag.isHeaderTag()) {
mutableParagraph.add(i + 1, MutableRange(paragraph.item, point.newIndex + 1, paragraph.end, paragraph.tag))
} else {
mutableParagraph.add(i + 1, MutableRange(ParagraphStyle(), point.newIndex + 1, paragraph.end, "p"))
mutableSpan.find {
it.start == paragraph.start && it.end == paragraph.end && it.tag == "h"
}?.let { it.end = point.newIndex + 1 }
}
paragraph.end = point.newIndex + 1
break
}
}
}
indexCorrect++
} else if (point.action == WordEditAction.delete) {
if (current.text[point.oldIndex] == '\n') {
val prevParagraph = mutableParagraph.find { it.end == point.oldIndex + 1 }
val nextParagraph = mutableParagraph.find { it.start == point.oldIndex + 1 && it.end != it.start }
nextParagraph?.let { np ->
prevParagraph?.let { pp ->
pp.end = np.end
mutableSpan.find { it.start == pp.start && it.tag == pp.tag }?.let {
it.end = np.end
}
}
mutableParagraph.remove(np)
mutableSpan.removeAll { np.start == it.start && it.tag == np.tag }
}
}
var i = 0
while (i < mutableSpan.size) {
val span = mutableSpan[i]
val shouldRemove = span.modifyByDelete(lastIndex)
if (shouldRemove) {
mutableSpan.removeAt(i)
i -= 1
}
i++
}
i = 0
while (i < mutableParagraph.size) {
val paragraph = mutableParagraph[i]
val shouldRemove = paragraph.modifyByDelete(lastIndex)
if (shouldRemove) {
mutableParagraph.removeAt(i)
i -= 1
}
i++
}
indexCorrect--
}
}
mutableSpan.removeAll {
it.start == it.end && (it.end < next.selection.start || it.start > next.selection.end)
}
mutableParagraph.removeAll {
it.start == it.end && (it.end < next.selection.start || it.start > next.selection.end)
}
val spanStyles = mutableSpan.map {
AnnotatedString.Range(it.item, it.start, it.end, it.tag)
}
val paragraphStyles = mutableParagraph.map {
AnnotatedString.Range(it.item, it.start, it.end, it.tag)
}
return TextFieldValue(
AnnotatedString(next.text, spanStyles, paragraphStyles),
next.selection,
next.composition
)
}
================================================
FILE: editor/src/main/java/com/qmuiteam/editor/Range.kt
================================================
package com.qmuiteam.editor
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
internal class MutableRange(
var item: T,
var start: Int,
var end: Int,
var tag: String
) {
fun modifyByInsert(insertPos: Int, appendIfAtEnd: Boolean) {
if (start == end) {
if (insertPos < start) {
start++
end++
} else if (insertPos == start) {
end++
}
} else {
if (insertPos < start) {
start++
end++
} else if (insertPos < end || (appendIfAtEnd && insertPos == end)) {
end++
}
}
}
fun modifyByDelete(deletePos: Int): Boolean {
if (start == end) {
if (deletePos < start - 1) {
start--
end--
} else if (deletePos == start - 1) {
start--
end--
return true
}
}
if (deletePos < start) {
start--
end--
} else if (deletePos < end) {
end--
}
return false
}
fun isCursorContained(cursorPos: Int): Boolean{
return if(start == end){
start == cursorPos
} else {
cursorPos in (start + 1) until end
}
}
}
fun AnnotatedString.Range.isCursorContained(cursorPos: Int): Boolean{
return if(start == end){
start == cursorPos
} else {
cursorPos in (start + 1)..end
}
}
================================================
FILE: editor/src/main/java/com/qmuiteam/editor/TextFieldValueEx.kt
================================================
package com.qmuiteam.editor
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.sp
//region ============== spanStyle ==============
fun TextFieldValue.bold(bold: BoldBehavior): TextFieldValue {
return textStyle(
style = SpanStyle(fontWeight = FontWeight(bold.weight)),
tag = bold.tag
) {
it.isBoldTag()
}
}
fun TextFieldValue.textColor(textColor: TextColorBehavior): TextFieldValue {
return textStyle(
style = SpanStyle(color = textColor.color),
tag = textColor.tag
) {
it.isBoldTag()
}
}
private fun TextFieldValue.textStyle(style: SpanStyle, tag: String, shouldHandle: (String) -> Boolean): TextFieldValue {
return modifySpans { spans ->
if (selection.collapsed) {
val contained = spans.find {
it.tag.isBoldTag() && it.isCursorContained(selection.start)
}
if (contained == null) {
spans.add(MutableRange(style, selection.start, selection.end, tag))
}
} else {
var i = 0
var handled = false
while (i < spans.size) {
val span = spans[i]
if (shouldHandle(span.tag)) {
if (span.start >= selection.start && span.end <= selection.end) {
spans.removeAt(i)
i--
} else if (span.end > selection.end && span.start < selection.start) {
if (span.tag == tag) {
handled = true
break
}
spans.add(i, MutableRange(span.item, selection.end, span.end, span.tag))
span.end = selection.start
i++
} else if (span.end > selection.start && span.start < selection.end) {
if (span.start >= selection.start) {
span.start = selection.end
}
if (span.end <= selection.end) {
span.end = selection.start
}
}
}
i++
}
if (!handled) {
spans.add(MutableRange(style, selection.start, selection.end, tag))
}
}
}
}
private fun TextFieldValue.modifySpans(
block: (spans: MutableList>) -> Unit
): TextFieldValue {
val mutableSpans = mutableListOf>()
annotatedString.spanStyles.forEach {
mutableSpans.add(MutableRange(it.item, it.start, it.end, it.tag))
}
block(mutableSpans)
val spanStyles = mutableSpans.map {
AnnotatedString.Range(it.item, it.start, it.end, it.tag)
}
return TextFieldValue(
AnnotatedString(text, spanStyles, annotatedString.paragraphStyles),
selection,
composition
)
}
//endregion
//region ============== paragraphStyle ==============
internal fun TextFieldValue.quote(): TextFieldValue {
return paragraphStyle(
ParagraphStyle(
textIndent = TextIndent(10.sp, 10.sp)
),
QuoteBehavior.tag
)
}
internal fun TextFieldValue.unOrder(): TextFieldValue {
return paragraphStyle(
ParagraphStyle(
textIndent = TextIndent(10.sp, 10.sp)
),
UnOrderListBehavior.tag
)
}
internal fun TextFieldValue.header(level: HeaderLevel): TextFieldValue {
return paragraphStyle(
ParagraphStyle(),
level.tag,
SpanStyle(fontSize = level.fontSize)
)
}
private fun MutableRange.replaceStyleIfNeeded(
value: TextFieldValue,
tag: String,
style: ParagraphStyle
): AnnotatedString.Range? {
if (value.selection.collapsed) {
val shouldModify = when {
start == end -> start == value.selection.start
value.selection.start in start until end -> true
value.selection.start == end -> value.text[end - 1] != '\n'
else -> false
}
if (shouldModify) {
if (this.tag != tag) {
val ret = AnnotatedString.Range(item, start, end)
this.item = style
this.tag = tag
return ret
}
}
} else {
if (start < value.selection.end && end > value.selection.start && this.tag != tag) {
val ret = AnnotatedString.Range(item, start, end)
this.item = style
this.tag = tag
return ret
}
}
return null
}
private fun TextFieldValue.paragraphStyle(
style: ParagraphStyle,
tag: String,
companionSpan: SpanStyle? = null
): TextFieldValue {
val replacedParagraphs = mutableListOf>()
val paragraphs = modifyParagraphs { paragraphs ->
paragraphs.forEach { paragraph ->
paragraph.replaceStyleIfNeeded(this, tag, style)?.let {
replacedParagraphs.add(it)
}
}
}
if (replacedParagraphs.isEmpty()) {
return paragraphs
}
return paragraphs.modifySpans { spans ->
spans.removeAll { span ->
replacedParagraphs.find {
it.start == span.start && it.end == span.end && it.tag == span.tag
} != null
}
replacedParagraphs.forEach { range ->
companionSpan?.let {
spans.add(MutableRange(it, range.start, range.end, tag))
}
}
}
}
private fun TextFieldValue.modifyParagraphs(
block: (paragraphs: MutableList>) -> Unit
): TextFieldValue {
val mutableParagraphs = mutableListOf>()
annotatedString.paragraphStyles.forEach {
mutableParagraphs.add(MutableRange(it.item, it.start, it.end, it.tag))
}
block(mutableParagraphs)
val paragraphStyles = mutableParagraphs.map {
AnnotatedString.Range(it.item, it.start, it.end, it.tag)
}
return TextFieldValue(
AnnotatedString(text, annotatedString.spanStyles, paragraphStyles),
selection,
composition
)
}
//endregion
internal fun TextFieldValue.check(): TextFieldValue {
val paragraphs = mutableListOf>()
var currentIndex = 0
var nextIndex = text.indexOf('\n')
while (nextIndex >= 0) {
val exist = annotatedString.paragraphStyles.find { it.start == currentIndex && it.end == nextIndex + 1 }
if (exist == null) {
paragraphs.add(AnnotatedString.Range(ParagraphStyle(), currentIndex, nextIndex + 1, NormalParagraphBehavior.tag))
} else {
paragraphs.add(exist)
}
currentIndex = nextIndex + 1
nextIndex = text.indexOf('\n', currentIndex)
}
if (currentIndex < text.length) {
val exist = annotatedString.paragraphStyles.find { it.start == currentIndex && it.end == text.length }
if (exist == null) {
paragraphs.add(AnnotatedString.Range(ParagraphStyle(), currentIndex, text.length, NormalParagraphBehavior.tag))
} else {
paragraphs.add(exist)
}
}
if (text.isEmpty() || (selection.collapsed && selection.end == text.length)) {
val exist = annotatedString.paragraphStyles.find { it.start == text.length && it.end == text.length }
if (exist == null) {
paragraphs.add(AnnotatedString.Range(ParagraphStyle(), text.length, text.length, NormalParagraphBehavior.tag))
} else {
paragraphs.add(exist)
}
}
return TextFieldValue(
AnnotatedString(text, annotatedString.spanStyles, paragraphs),
selection,
composition
)
}
================================================
FILE: editor/src/main/java/com/qmuiteam/editor/WordEdit.kt
================================================
package com.qmuiteam.editor
import androidx.compose.ui.text.input.TextFieldValue
import java.util.*
enum class WordEditAction {
insert, delete, repace
}
data class WordEditPoint(
val action: WordEditAction,
val oldIndex: Int,
val newIndex: Int
)
class WordEditResult(
val dis: Int,
val list: List
)
private class WordEditRecordNode(val point: WordEditPoint) {
var prev: WordEditRecordNode? = null
}
private class WordEditRecord(
var dis: Int
) {
var node: WordEditRecordNode? = null
}
fun wordEdit(oldTextFieldValue: TextFieldValue, newTextFieldValue: TextFieldValue): WordEditResult {
val oldText = oldTextFieldValue.text
val newText = newTextFieldValue.text
if(oldText.length <= 20 || newText.length <= 20){
return wordEdit(oldText, newText)
}
var prefixCheckLength = 10
var prefix = (oldTextFieldValue.selection.start - prefixCheckLength)
.coerceAtMost(newTextFieldValue.selection.start - prefixCheckLength)
.coerceAtLeast(0)
while (prefix > 0){
if(oldText.substring(0, prefix) == newText.substring(0, prefix)){
break
}
prefixCheckLength *= 2
prefix = (prefix - prefixCheckLength).coerceAtLeast(0)
}
var suffixCheckLength = 10
var suffix = (oldText.length - oldTextFieldValue.selection.end - suffixCheckLength)
.coerceAtMost(newText.length - newTextFieldValue.selection.end - suffixCheckLength)
.coerceAtLeast(0)
while (suffix > 0){
if(oldText.substring(oldText.length - suffix) == newText.substring(newText.length - suffix)){
break
}
suffixCheckLength *= 2
suffix = (suffix - suffixCheckLength).coerceAtLeast(0)
}
if(prefix == 0 && suffix == 0){
return wordEdit(oldText, newText)
}
return wordEdit(
oldText.substring(prefix, oldText.length - suffix),
newText.substring(prefix, newText.length - suffix)
)
}
fun wordEdit(oldText: String, newText: String, shift: Int = 0): WordEditResult {
val array = arrayOfNulls(oldText.length + 1)
val next = arrayOfNulls