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 ================================================

Banner

# QMUI_Android QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 [![QMUI Team Name](https://img.shields.io/badge/Team-QMUI-brightgreen.svg?style=flat)](https://github.com/QMUI "QMUI Team") [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](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 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 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 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 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 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) 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 cls = (Class) 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 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 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 targetActivity, @NonNull Class 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 targetActivity, @NonNull Class 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 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 activity = (Class) activityCls; Class fragment = (Class) 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 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 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 cls = (Class) 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 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 mEffectType; EffectHandlerWrapper(QMUIFragmentEffectHandler handler, Lifecycle lifecycle) { mHandler = handler; mLifecycle = lifecycle; lifecycle.addObserver(this); mEffectType = getHandlerEffectType(handler); } @SuppressWarnings("unchecked") private Class getHandlerEffectType(QMUIFragmentEffectHandler handler) { Class 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) 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 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 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 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 map = annotationMirror.getElementValues(); for (Map.Entry 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 annotations, RoundEnvironment roundEnv) { Set 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 annotations, RoundEnvironment roundEnv) { Set activitySchemes = roundEnv.getElementsAnnotatedWith(ActivityScheme.class); Set 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 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 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 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(oldText.length + 1) for (j in array.indices) { array[j] = WordEditRecord(j).apply { if (j > 0) { node = WordEditRecordNode( WordEditPoint( WordEditAction.delete, shift + j - 1, -1 ) ).apply { prev = array[j - 1]!!.node } } } } for (i in newText.indices) { for (j in array.indices) { val columnLast = array[j]!! if (j == 0) { next[j] = WordEditRecord(columnLast.dis + 1).apply { node = WordEditRecordNode( WordEditPoint(WordEditAction.insert, shift + j - 1, shift + i) ).apply { prev = columnLast.node } } } else { val path1 = WordEditRecord(columnLast.dis + 1).apply { node = WordEditRecordNode( WordEditPoint(WordEditAction.insert, shift + j - 1, shift + i) ).apply { prev = columnLast.node } } val rowLast = next[j - 1]!! val path2 = WordEditRecord(rowLast.dis + 1).apply { node = WordEditRecordNode( WordEditPoint(WordEditAction.delete, shift + j - 1, -1) ).apply { prev = rowLast.node } } val diagonalLast = array[j - 1]!! val path3 = if (newText[i] == oldText[j - 1]) { diagonalLast } else { WordEditRecord(diagonalLast.dis + 1).apply { node = WordEditRecordNode( WordEditPoint( WordEditAction.repace, j - 1, i ) ).apply { prev = diagonalLast.node } } } var minPath = path1 if (path2.dis < minPath.dis) { minPath = path2 } if (path3.dis < minPath.dis) { minPath = path3 } next[j] = minPath } } for (j in array.indices) { array[j] = next[j] } } val ret = array[array.size - 1]!! val list = LinkedList() var node = ret.node while (node != null) { list.addFirst(node!!.point) node = node?.prev } return WordEditResult(ret.dis, list) } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Jun 11 14:18:59 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip ================================================ FILE: gradle.properties ================================================ # suppress inspection "UnusedProperty" for whole file # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.injected.testOnly=false android.useAndroidX=true android.disableAutomaticComponentCreation=true android.defaults.buildfeatures.buildconfig=false android.defaults.buildfeatures.aidl=false android.defaults.buildfeatures.shaders=false GROUP=com.qmuiteam QMUI_VERSION=2.0.1 QMUI_ARCH_VERSION=2.0.1 QMUI_TYPE_VERSION = 0.0.14 POM_GIT_URL=https://github.com/Tencent/QMUI_Android/ POM_SITE_URL=https://qmuiteam.com/android ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: lib/.gitignore ================================================ /build /*.iml ================================================ FILE: lib/build.gradle.kts ================================================ import com.qmuiteam.plugin.Dep plugins { `java-library` } java { sourceCompatibility = Dep.javaVersion targetCompatibility = Dep.javaVersion } ================================================ FILE: lib/src/main/java/com/qmuiteam/qmuidemo/lib/Group.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.lib; /** * @author cginechen * @date 2016-12-14 */ public enum Group { Component, Helper, Lab, Other } ================================================ FILE: lib/src/main/java/com/qmuiteam/qmuidemo/lib/annotation/Widget.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.lib.annotation; import com.qmuiteam.qmuidemo.lib.Group; 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 Widget { Group group() default Group.Component; Class widgetClass() default void.class; String name() default ""; String docUrl() default ""; int iconRes() default 0; } ================================================ FILE: photo/.gitignore ================================================ /build ================================================ FILE: photo/build.gradle.kts ================================================ import com.qmuiteam.plugin.Dep plugins { id("com.android.library") kotlin("android") `maven-publish` signing id("qmui-publish") } version = Dep.QMUI.photoVer 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(Dep.AndroidX.appcompat) api(project(":compose-core")) implementation(Dep.AndroidX.activity) implementation(Dep.Compose.activity) implementation(Dep.Compose.pager) implementation(Dep.Compose.constraintlayout) } ================================================ FILE: photo/consumer-rules.pro ================================================ ================================================ FILE: photo/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: photo/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt ================================================ package com.qmuiteam 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.test", appContext.packageName) } } ================================================ FILE: photo/src/main/AndroidManifest.xml ================================================ ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoClipActivity.kt ================================================ package com.qmuiteam.photo.activity import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.qmuiteam.compose.core.provider.QMUIWindowInsetsProvider import com.qmuiteam.photo.compose.QMUIPhotoClipper import com.qmuiteam.photo.data.QMUIPhotoProvider import com.qmuiteam.photo.util.saveToLocal import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal const val QMUI_PHOTO_CLIP_URI = "qmui_photo_clip_uri" internal const val QMUI_PHOTO_CLIP_SOURCE_RATIO = "qmui_photo_clip_source_ratio" fun Intent.getQMUIPhotoClipResult(): Uri? { return getParcelableExtra(QMUI_PHOTO_CLIP_URI) } abstract class QMUIPhotoClipActivity : AppCompatActivity() { companion object { fun intentOf( activity: ComponentActivity, cls: Class, sourceUri: Uri, sourceRatio: Float = -1f ): Intent { val intent = Intent(activity, cls) intent.putExtra(QMUI_PHOTO_CLIP_URI, sourceUri) intent.putExtra(QMUI_PHOTO_CLIP_SOURCE_RATIO, sourceRatio) return intent } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.getInsetsController(window, window.decorView)?.let { it.isAppearanceLightNavigationBars = false } window.statusBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { window.navigationBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { window.navigationBarDividerColor = android.graphics.Color.TRANSPARENT window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } val uri = intent.getParcelableExtra(QMUI_PHOTO_CLIP_URI) if (uri == null) { finish() return } val ratio = intent.getFloatExtra(QMUI_PHOTO_CLIP_SOURCE_RATIO, -1f) setContent { PageContent(uri, ratio) } } @Composable protected abstract fun photoProvider(uri: Uri, ratio: Float): QMUIPhotoProvider @Composable protected open fun PageContent(uri: Uri, ratio: Float) { Box(modifier = Modifier.background(Color.Black)) { QMUIWindowInsetsProvider { QMUIPhotoClipper( photoProvider = photoProvider(uri, ratio) ) { doClip -> Row( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) ) { Box(modifier = Modifier .weight(1f) .clickable { finish() } .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { Text( "取消", fontSize = 20.sp, color = Color.White, fontWeight = FontWeight.Bold ) } Box(modifier = Modifier .weight(1f) .clickable { doClip()?.let { handleResult(it) } } .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { Text( "确定", fontSize = 20.sp, color = Color.White, fontWeight = FontWeight.Bold ) } } } } } } protected open fun handleResult(bitmap: Bitmap) { lifecycleScope.launch { val ret = kotlin.runCatching { withContext(Dispatchers.IO) { bitmap.saveToLocal(cacheDir) } }.getOrNull() setResult(RESULT_OK, Intent().apply { putExtra(QMUI_PHOTO_CLIP_URI, ret) }) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoPickerActivity.kt ================================================ package com.qmuiteam.photo.activity import android.Manifest import android.app.Application import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcel import android.os.Parcelable import android.util.Log import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import com.qmuiteam.compose.core.helper.QMUIGlobal import com.qmuiteam.compose.core.provider.QMUIWindowInsetsProvider import com.qmuiteam.compose.core.ui.QMUITopBar import com.qmuiteam.compose.core.ui.QMUITopBarBackIconItem import com.qmuiteam.compose.core.ui.QMUITopBarItem import com.qmuiteam.compose.core.ui.QMUITopBarWithLazyScrollState import com.qmuiteam.photo.compose.* import com.qmuiteam.photo.compose.picker.* import com.qmuiteam.photo.data.* import com.qmuiteam.photo.vm.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch const val QMUI_PHOTO_DEFAULT_PICK_LIMIT_COUNT = 9 internal const val QMUI_PHOTO_RESULT_URI_LIST = "qmui_photo_result_uri_list" internal const val QMUI_PHOTO_RESULT_ORIGIN_OPEN = "qmui_photo_result_origin_open" internal const val QMUI_PHOTO_ENABLE_ORIGIN = "qmui_photo_enable_origin" internal const val QMUI_PHOTO_PICK_LIMIT_COUNT = "qmui_photo_pick_limit_count" internal const val QMUI_PHOTO_PICKED_ITEMS = "qmui_photo_picked_items" internal const val QMUI_PHOTO_PROVIDER_FACTORY = "qmui_photo_provider_factory" class QMUIPhotoPickItemInfo( val id: Long, val name: String, val width: Int, val height: Int, val uri: Uri, val rotation: Int ) : Parcelable { fun ratio(): Float { if(height <= 0 || width <= 0){ return -1f } if(rotation == 90 || rotation == 270){ return height.toFloat() / width } return width.toFloat() / height } constructor(parcel: Parcel) : this( parcel.readLong(), parcel.readString()!!, parcel.readInt(), parcel.readInt(), parcel.readParcelable(Uri::class.java.classLoader)!!, parcel.readInt() ) override fun describeContents(): Int { return 0 } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeLong(id) dest.writeString(name) dest.writeInt(width) dest.writeInt(height) dest.writeParcelable(uri, flags) dest.writeInt(rotation) } companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): QMUIPhotoPickItemInfo { return QMUIPhotoPickItemInfo(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } } class QMUIPhotoPickResult(val list: List, val isOriginOpen: Boolean) fun Intent.getQMUIPhotoPickResult(): QMUIPhotoPickResult? { val list = getParcelableArrayListExtra(QMUI_PHOTO_RESULT_URI_LIST) ?: return null if (list.isEmpty()) { return null } val isOriginOpen = getBooleanExtra(QMUI_PHOTO_RESULT_ORIGIN_OPEN, false) return QMUIPhotoPickResult(list, isOriginOpen) } open class QMUIPhotoPickerActivity : AppCompatActivity() { companion object { fun intentOf( activity: ComponentActivity, cls: Class, factoryCls: Class, pickedItems: ArrayList = arrayListOf(), pickLimitCount: Int = QMUI_PHOTO_DEFAULT_PICK_LIMIT_COUNT, enableOrigin: Boolean = true ): Intent { val intent = Intent(activity, cls) intent.putExtra(QMUI_PHOTO_PICK_LIMIT_COUNT, pickLimitCount) intent.putParcelableArrayListExtra(QMUI_PHOTO_PICKED_ITEMS, pickedItems) intent.putExtra(QMUI_PHOTO_PROVIDER_FACTORY, factoryCls.name) intent.putExtra(QMUI_PHOTO_ENABLE_ORIGIN, enableOrigin) return intent } } private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { onHandlePermissionResult(it) } private val viewModel by viewModels(factoryProducer = { object : AbstractSavedStateViewModelFactory(this@QMUIPhotoPickerActivity, intent?.extras) { override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { val constructor = modelClass.getDeclaredConstructor( Application::class.java, SavedStateHandle::class.java, QMUIMediaDataProvider::class.java, Array::class.java ) return constructor.newInstance( this@QMUIPhotoPickerActivity.application, handle, dataProvider(), supportedMimeTypes() ) } } }) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.getInsetsController(window, window.decorView)?.let { it.isAppearanceLightNavigationBars = false } window.statusBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { window.navigationBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { window.navigationBarDividerColor = android.graphics.Color.TRANSPARENT window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } setContent { PageContent(viewModel) } onStartCheckPermission() onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { when (viewModel.photoPickerSceneFlow.value) { is QMUIPhotoPickerEditScene -> { viewModel.updateScene(viewModel.prevScene ?: QMUIPhotoPickerGridScene) } is QMUIPhotoPickerPreviewScene -> { viewModel.updateScene(QMUIPhotoPickerGridScene) } else -> { isEnabled = false onBackPressed() isEnabled = true } } } }) } @Composable protected open fun PageContent(viewModel: QMUIPhotoPickerViewModel) { QMUIDefaultPickerConfigProvider { QMUIWindowInsetsProvider { Box( modifier = Modifier .fillMaxSize() .background(QMUILocalPickerConfig.current.screenBgColor) ) { PhotoPicker(viewModel) } } } } @Composable protected open fun BoxScope.PhotoPicker(viewModel: QMUIPhotoPickerViewModel) { val data by viewModel.photoPickerDataFlow.collectAsState() when (data.state) { QMUIPhotoPickerLoadState.dataLoading, QMUIPhotoPickerLoadState.permissionChecking -> { Loading() } QMUIPhotoPickerLoadState.permissionDenied -> { PermissionDenied() } QMUIPhotoPickerLoadState.dataLoaded -> { val error = data.error val list = data.data if (error != null) { PageError(error) } else if (list == null || list.isEmpty()) { PageEmpty() } else { PhotoPickerContent(viewModel, list) } } } } @OptIn(ExperimentalAnimationApi::class) @Composable protected open fun BoxScope.PhotoPickerContent( viewModel: QMUIPhotoPickerViewModel, data: List ) { val pickedItems by viewModel.pickedListFlow.collectAsState() val sceneState = viewModel.photoPickerSceneFlow.collectAsState() val scene = sceneState.value AnimatedVisibility( visible = scene is QMUIPhotoPickerGridScene, enter = fadeIn(), exit = fadeOut() ) { PhotoPickerGridScene(viewModel, data, pickedItems) } AnimatedVisibility( visible = scene is QMUIPhotoPickerPreviewScene, enter = if(viewModel.prevScene !is QMUIPhotoPickerEditScene) fadeIn() + scaleIn(initialScale = 0.8f) else fadeIn(initialAlpha = 1f), exit = if(scene !is QMUIPhotoPickerEditScene) fadeOut() + scaleOut(targetScale = 0.8f) else fadeOut(targetAlpha = 1f) ) { // For exit animation val previewSceneHolder = remember { SceneHolder(scene as? QMUIPhotoPickerPreviewScene) } if(scene is QMUIPhotoPickerPreviewScene){ previewSceneHolder.scene = scene } val previewScene = previewSceneHolder.scene if (previewScene != null) { PhotoPickerPreviewScene(viewModel, previewScene, data, pickedItems) } } AnimatedVisibility( visible = scene is QMUIPhotoPickerEditScene, enter = fadeIn() + scaleIn(initialScale = 0.8f), exit = fadeOut() + scaleOut(targetScale = 0.8f) ) { val editSceneHolder = remember { SceneHolder(scene as? QMUIPhotoPickerEditScene) } if(scene is QMUIPhotoPickerEditScene){ editSceneHolder.scene = scene } val editScene = editSceneHolder.scene if (editScene != null) { PhotoPickerEditScene(viewModel, editScene) } } } @Composable protected open fun BoxScope.PhotoPickerGridScene( viewModel: QMUIPhotoPickerViewModel, data: List, pickedItems: List, topBarBackItem: QMUITopBarItem = remember { QMUITopBarBackIconItem { finish() } } ) { LaunchedEffect("") { WindowCompat.getInsetsController(window, window.decorView)?.show(WindowInsetsCompat.Type.statusBars()) } var currentBucket by remember { mutableStateOf(data.first()) } val scrollState = viewModel.gridSceneScrollState val bucketFlow = remember { MutableStateFlow(currentBucket.name) }.apply { value = currentBucket.name } val isFocusBucketFlow = remember { MutableStateFlow(false) } val config = QMUILocalPickerConfig.current val topBarBucketItem = remember(config) { config.topBarBucketFactory(bucketFlow, isFocusBucketFlow) { isFocusBucketFlow.value = !isFocusBucketFlow.value } } val isFocusBucketChooser by isFocusBucketFlow.collectAsState() val topBarSendItem = remember(config) { config.topBarSendFactory(false, viewModel.pickLimitCount, viewModel.pickedCountFlow) { onHandleSend(viewModel.getPickedResultList()) } } val topBarLeftItems = remember(topBarBackItem, topBarBucketItem) { arrayListOf(topBarBackItem, topBarBucketItem) } val topBarRightItems = remember(topBarSendItem) { arrayListOf(topBarSendItem) } Column(modifier = Modifier.fillMaxSize()) { QMUITopBarWithLazyScrollState( scrollState = scrollState, paddingEnd = 16.dp, separatorHeight = 0.dp, backgroundColor = QMUILocalPickerConfig.current.topBarBgColor, leftItems = topBarLeftItems, rightItems = topBarRightItems ) ConstraintLayout( modifier = Modifier .fillMaxWidth() .weight(1f) ) { val (content, toolbar) = createRefs() QMUIPhotoPickerGrid( data = currentBucket.list, modifier = Modifier.constrainAs(content) { width = Dimension.fillToConstraints height = Dimension.fillToConstraints top.linkTo(parent.top) start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(toolbar.top) }, state = scrollState, pickedItems = pickedItems, onPickItem = { _, model -> viewModel.togglePick(model) }, onPreview = { viewModel.updateScene(QMUIPhotoPickerPreviewScene(currentBucket.id, false, it.id)) } ) QMUIPhotoPickerGridToolBar( modifier = Modifier .constrainAs(toolbar) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(parent.bottom) }, enableOrigin = viewModel.enableOrigin, pickedItems = pickedItems, isOriginOpenFlow = viewModel.isOriginOpenFlow, onToggleOrigin = { viewModel.toggleOrigin(it) } ) { viewModel.updateScene(QMUIPhotoPickerPreviewScene(currentBucket.id, true, currentBucket.list.first().model.id)) } QMUIPhotoBucketChooser( focus = isFocusBucketChooser, data = data, currentId = currentBucket.id, onBucketClick = { currentBucket = it isFocusBucketFlow.value = false }) { isFocusBucketFlow.value = false } } } } @Composable protected open fun BoxScope.PhotoPickerPreviewScene( viewModel: QMUIPhotoPickerViewModel, scene: QMUIPhotoPickerPreviewScene, data: List, pickedItems: List ) { val list = remember(scene) { if (scene.onlySelected) { viewModel.getPickedVOList() } else { data.find { it.id == scene.buckedId }?.list ?: emptyList() } } PhotoPickerPreviewContent(viewModel, list, pickedItems, scene) } @OptIn(ExperimentalPagerApi::class) @Composable protected open fun BoxScope.PhotoPickerPreviewContent( viewModel: QMUIPhotoPickerViewModel, data: List, pickedItems: List, scene: QMUIPhotoPickerPreviewScene ) { val config = QMUILocalPickerConfig.current var isFullPageState by remember { mutableStateOf(false) } LaunchedEffect(isFullPageState) { WindowCompat.getInsetsController(window, window.decorView)?.let { if (!isFullPageState) { it.show(WindowInsetsCompat.Type.statusBars()) } else { it.hide(WindowInsetsCompat.Type.statusBars()) } } } val pagerState = remember(data, scene.currentId) { PagerState( currentPage = data.indexOfFirst { it.model.id == scene.currentId }.coerceAtLeast(0), ) } val topBarLeftItems = remember { arrayListOf(QMUITopBarBackIconItem { viewModel.updateScene(QMUIPhotoPickerGridScene) }) } val topBarRightItems = remember(config) { arrayListOf(config.topBarSendFactory(true, viewModel.pickLimitCount, viewModel.pickedCountFlow) { val pickedList = viewModel.getPickedResultList() if(pickedList.isEmpty()){ onHandleSend(listOf(data[pagerState.currentPage].let { QMUIPhotoPickItemInfo( it.model.id, it.model.name, it.model.width, it.model.height, it.model.uri, it.model.rotation ) })) }else{ onHandleSend(pickedList) } }) } val scope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { QMUIPhotoPickerPreview( pagerState, data, loading = { Loading() }, loadingFailed = {}, ) { isFullPageState = !isFullPageState } AnimatedVisibility( visible = !isFullPageState, enter = slideInVertically(initialOffsetY = { -it }), exit = slideOutVertically(targetOffsetY = { -it }) ) { QMUITopBar( title = "${pagerState.currentPage + 1}/${data.size}", separatorHeight = 0.dp, paddingEnd = 16.dp, backgroundColor = QMUILocalPickerConfig.current.topBarBgColor, leftItems = topBarLeftItems, rightItems = topBarRightItems ) } AnimatedVisibility( visible = !isFullPageState, modifier = Modifier.align(Alignment.BottomCenter), enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }) ) { Column(modifier = Modifier.fillMaxWidth()) { QMUIPhotoPickerPreviewPickedItems(data, pickedItems, data[pagerState.currentPage].model.id) { scope.launch { pagerState.scrollToPage(data.indexOf(it)) } } val isCurrentPicked = remember(data, pickedItems, pagerState.currentPage) { pickedItems.indexOf(data[pagerState.currentPage].model.id) >= 0 } QMUIPhotoPickerPreviewToolBar( modifier = Modifier.fillMaxWidth(), current = data[pagerState.currentPage], isCurrentPicked = isCurrentPicked, enableOrigin = viewModel.enableOrigin, isOriginOpenFlow = viewModel.isOriginOpenFlow, onToggleOrigin = { viewModel.toggleOrigin(it) }, onEdit = { viewModel.updateScene(QMUIPhotoPickerEditScene(data[pagerState.currentPage])) }, onToggleSelect = { viewModel.togglePick(data[pagerState.currentPage]) } ) } } } } @Composable protected open fun BoxScope.PhotoPickerEditScene( viewModel: QMUIPhotoPickerViewModel, scene: QMUIPhotoPickerEditScene ) { LaunchedEffect("") { WindowCompat.getInsetsController(window, window.decorView)?.hide(WindowInsetsCompat.Type.statusBars()) } QMUIPhotoPickerEdit(onBackPressedDispatcher, scene.current) { viewModel.updateScene(viewModel.prevScene ?: QMUIPhotoPickerGridScene) } } @Composable protected open fun BoxScope.Loading() { Box(modifier = Modifier.align(Alignment.Center)) { QMUIPhotoLoading(lineColor = QMUILocalPickerConfig.current.loadingColor) } } @Composable protected open fun BoxScope.PermissionDenied() { CommonTip(text = "选择图片需要存储权限\n请先前往设置打开存储权限") } @Composable protected open fun BoxScope.PageError(throwable: Throwable) { val text = if (QMUIGlobal.debug) { "读取数据发生错误, ${throwable.message}" } else { "读取数据发生错误" } CommonTip(text = text) } @Composable protected open fun BoxScope.PageEmpty() { CommonTip(text = "你的相册空空如也~") } @Composable protected open fun BoxScope.CommonTip(text: String) { Box( modifier = Modifier .align(Alignment.Center) .padding(20.dp) ) { Text( text, fontSize = 16.sp, color = QMUILocalPickerConfig.current.tipTextColor, textAlign = TextAlign.Center, lineHeight = 20.sp ) } } protected open fun onHandleSend(pickedList: List) { setResult(RESULT_OK, Intent().apply { putParcelableArrayListExtra(QMUI_PHOTO_RESULT_URI_LIST, arrayListOf().apply { addAll(pickedList) }) putExtra(QMUI_PHOTO_RESULT_ORIGIN_OPEN, viewModel.isOriginOpenFlow.value) }) finish() } protected open fun onStartCheckPermission() { permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } protected open fun onHandlePermissionResult(granted: Boolean) { if (granted) { viewModel.permissionGranted() } else { viewModel.permissionDenied() } } protected open fun dataProvider(): QMUIMediaDataProvider { return QMUIMediaImagesProvider() } protected open fun supportedMimeTypes(): Array { return QMUIMediaImagesProvider.DEFAULT_SUPPORT_MIMETYPES } private class SceneHolder(var scene: T? = null) } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.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.photo.activity import android.content.Intent import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.util.Log import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.core.Transition import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.PagerScope import com.google.accompanist.pager.rememberPagerState import com.qmuiteam.compose.core.helper.QMUILog import com.qmuiteam.photo.R import com.qmuiteam.photo.compose.QMUIDefaultPhotoConfigProvider import com.qmuiteam.photo.compose.QMUIGesturePhoto import com.qmuiteam.photo.compose.QMUIPhotoLoading import com.qmuiteam.photo.data.* import com.qmuiteam.photo.util.asBitmap import kotlinx.coroutines.flow.MutableStateFlow private const val PHOTO_CURRENT_INDEX = "qmui_photo_current_index" private const val PHOTO_TRANSITION_DELIVERY_KEY = "qmui_photo_transition_delivery" private const val PHOTO_COUNT = "qmui_photo_count" private const val PHOTO_META_KEY_PREFIX = "qmui_photo_meta_" private const val PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX = "qmui_photo_provider_recover_cls_" open class QMUIPhotoViewerActivity : AppCompatActivity() { companion object { fun intentOf( activity: ComponentActivity, cls: Class, list: List, index: Int ): Intent { val data = PhotoViewerData(list, index, activity.window.decorView.asBitmap()) val intent = Intent(activity, cls) intent.putExtra(PHOTO_TRANSITION_DELIVERY_KEY, QMUIPhotoTransitionDelivery.put(data)) intent.putExtra(PHOTO_CURRENT_INDEX, index) intent.putExtra(PHOTO_COUNT, list.size) if(list.size < 250){ list.forEachIndexed { i, transition -> val meta = transition.photoProvider.meta() val recoverCls = transition.photoProvider.recoverCls() if (meta != null && recoverCls != null) { intent.putExtra("${PHOTO_META_KEY_PREFIX}${i}", meta) intent.putExtra( "${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}", recoverCls.name ) } } } else { QMUILog.w("QMUIPhotoViewerActivity", "once delivered too many photos, so only use memory data for delivery, there may be some recover issue.") } return intent } } private val viewModel by viewModels() private val transitionTargetFlow = MutableStateFlow(true) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.getInsetsController(window, window.decorView)?.let { it.hide(WindowInsetsCompat.Type.statusBars()) it.isAppearanceLightNavigationBars = false } if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { window.navigationBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { window.navigationBarDividerColor = android.graphics.Color.TRANSPARENT window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { transitionTargetFlow.value = false } }) setContent { PageContent() } } @Composable protected open fun PageContent() { Box( modifier = Modifier.fillMaxSize() ) { val data = viewModel.data if (data == null || data.list.isEmpty()) { Text(text = "没有图片数据") } else { viewModel.data?.background?.let { Image( painter = BitmapPainter(it.asImageBitmap()), contentDescription = "", contentScale = ContentScale.FillWidth, alignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize() ) } PhotoViewerProviderWrapper(list = data.list, index = data.index) } } } @Composable protected open fun PhotoViewerProviderWrapper(list: List, index: Int) { QMUIDefaultPhotoConfigProvider { PhotoViewer(list, index) } } @OptIn(ExperimentalPagerApi::class) @Composable protected open fun PhotoViewer(list: List, index: Int) { val pagerState = rememberPagerState(index) HorizontalPager( count = list.size, state = pagerState ) { page -> PhotoPage(page, list[page], page == index) } } protected open fun pullExitMiniTranslateY(): Dp = 72.dp @OptIn(ExperimentalPagerApi::class) @Composable protected open fun PagerScope.PhotoPage(page: Int, item: QMUIPhotoTransitionInfo, shouldTransitionEnter: Boolean) { val initRect = item.photoRect() val transitionTarget = if (currentPage == page) { transitionTargetFlow.collectAsState().value } else true val drawableCache = remember { MutableDrawableCache() } BoxWithConstraints(modifier = Modifier.fillMaxSize()) { PhotoGestureWrapper(item) { photoLoadCallback -> QMUIGesturePhoto( containerWidth = maxWidth, containerHeight = maxHeight, imageRatio = item.ratio(), isLongImage = item.photoProvider.isLongImage(), initRect = initRect, shouldTransitionEnter = shouldTransitionEnter && shouldTransitionPhoto(), shouldTransitionExit = shouldTransitionPhoto(), transitionTarget = transitionTarget, pullExitMiniTranslateY = pullExitMiniTranslateY(), onBeginPullExit = { allowPullExit() }, onLongPress = { drawableCache.drawable?.let { onLongClick(page, it) } }, onTapExit = { onTapExit(page, it) } ) { transition, _, _, onImageRatioEnsured -> val onPhotoLoad: (PhotoResult) -> Unit = remember(drawableCache, onImageRatioEnsured) { { drawableCache.drawable = it.drawable if (it.drawable.intrinsicWidth > 0 && it.drawable.intrinsicHeight > 0) { onImageRatioEnsured(it.drawable.intrinsicWidth.toFloat() / it.drawable.intrinsicHeight) } photoLoadCallback?.invoke(it) } } PhotoContent( transition = transition, photoTransitionInfo = item, onPhotoLoaded = onPhotoLoad ) } } } } @Composable protected open fun BoxWithConstraintsScope.PhotoGestureWrapper( item: QMUIPhotoTransitionInfo, content: @Composable BoxWithConstraintsScope.(onPhotoLoaded: ((PhotoResult) -> Unit)?)->Unit ){ content(null) } @Composable protected open fun PhotoContent( transition: Transition, photoTransitionInfo: QMUIPhotoTransitionInfo, onPhotoLoaded: (PhotoResult) -> Unit ) { DefaultPhotoContent(transition, photoTransitionInfo, onPhotoLoaded) } @Composable protected fun DefaultPhotoContent( transition: Transition, photoTransitionInfo: QMUIPhotoTransitionInfo, onPhotoLoaded: (PhotoResult) -> Unit ){ val thumb = remember(photoTransitionInfo) { photoTransitionInfo.photoProvider.thumbnail(false) } var loadStatus by remember { mutableStateOf(PhotoLoadStatus.loading) } val onSuccess: (PhotoResult) -> Unit = remember(onPhotoLoaded) { { onPhotoLoaded(it) loadStatus = PhotoLoadStatus.success } } Box(modifier = Modifier.fillMaxSize()) { PhotoItem(photoTransitionInfo, onSuccess = onSuccess, onError = { loadStatus = PhotoLoadStatus.failed } ) if (loadStatus != PhotoLoadStatus.success || !transition.currentState || !transition.targetState) { val transitionPhoto = photoTransitionInfo.photo val contentScale = when { photoTransitionInfo.photoProvider.isLongImage() -> { ContentScale.FillWidth } photoTransitionInfo.ratio() > 0f && photoTransitionInfo.offsetInWindow != null && photoTransitionInfo.size != null -> { ContentScale.Crop } else -> ContentScale.Fit } if (transitionPhoto != null) { Image( painter = BitmapPainter(transitionPhoto.toBitmap().asImageBitmap()), contentDescription = "", alignment = if (photoTransitionInfo.photoProvider.isLongImage()) Alignment.TopCenter else Alignment.Center, contentScale = contentScale, modifier = Modifier.fillMaxSize() ) } else { thumb?.Compose( contentScale = contentScale, isContainerDimenExactly = true, onSuccess = null, onError = null ) } } if (loadStatus == PhotoLoadStatus.loading) { Loading() } else if (loadStatus == PhotoLoadStatus.failed) { LoadingFailed() } } } @Composable private fun PhotoItem( photoTransitionInfo: QMUIPhotoTransitionInfo, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? = null ) { val photo = remember(photoTransitionInfo) { photoTransitionInfo.photoProvider.photo() } photo?.Compose( contentScale = ContentScale.Fit, isContainerDimenExactly = true, onSuccess = onSuccess, onError = onError ) } @Composable protected open fun BoxScope.Loading() { Box(modifier = Modifier.align(Alignment.Center)) { QMUIPhotoLoading(size = 48.dp) } } @Composable protected open fun BoxScope.LoadingFailed() { // do nothing default, users should handle load fail / reload in Photo } protected open fun shouldTransitionPhoto(): Boolean { return true } protected open fun allowPullExit(): Boolean { return true } protected open fun onLongClick(page: Int, drawable: Drawable) { } protected open fun onTapExit(page: Int, afterTransition: Boolean) { if (afterTransition) { finish() overridePendingTransition(0, 0) } else { finish() overridePendingTransition(0, R.anim.scale_exit) } } } class QMUIPhotoViewerViewModel(val state: SavedStateHandle) : ViewModel() { val enterIndex = state.get(PHOTO_CURRENT_INDEX) ?: 0 val data: PhotoViewerData? private val transitionDeliverKey = state.get(PHOTO_TRANSITION_DELIVERY_KEY) ?: -1 init { val transitionDeliverData = QMUIPhotoTransitionDelivery.getAndRemove(transitionDeliverKey) data = if (transitionDeliverData != null) { transitionDeliverData } else { val count = state.get(PHOTO_COUNT) ?: 0 if (count > 0) { val list = arrayListOf() for (i in 0 until count) { try { val meta = state.get("${PHOTO_META_KEY_PREFIX}${i}") val clsName = state.get("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}") if (meta == null || clsName.isNullOrBlank()) { list.add(lossPhotoTransitionInfo) } else { val cls = Class.forName(clsName) val recover = cls.newInstance() as PhotoTransitionProviderRecover list.add(recover.recover(meta) ?: lossPhotoTransitionInfo) } } catch (e: Throwable) { list.add(lossPhotoTransitionInfo) } } PhotoViewerData(list, enterIndex, null) } else { null } } } override fun onCleared() { super.onCleared() QMUIPhotoTransitionDelivery.remove(transitionDeliverKey) } } class MutableDrawableCache(var drawable: Drawable? = null) ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt ================================================ package com.qmuiteam.photo.compose import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import com.qmuiteam.photo.data.QMUIBitmapRegionProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable fun QMUIBitmapRegionItem(bmRegion: QMUIBitmapRegionProvider, w: Dp, h: Dp) { var bitmap by remember { mutableStateOf(null) } LaunchedEffect(key1 = bmRegion) { withContext(Dispatchers.IO) { bitmap = bmRegion.loader.load() } } Box(modifier = Modifier.size(w, h)) { val bm = bitmap if (bm != null) { Image( painter = BitmapPainter(bm.asImageBitmap()), contentDescription = "", contentScale = ContentScale.FillWidth, modifier = Modifier.fillMaxSize() ) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/GesturePhoto.kt ================================================ package com.qmuiteam.photo.compose import android.util.Log import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.runtime.* 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.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.* 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.input.pointer.consumeAllChanges import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChangeConsumed import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.absoluteValue @Composable fun QMUIGesturePhoto( containerWidth: Dp, containerHeight: Dp, imageRatio: Float, isLongImage: Boolean, initRect: Rect? = null, shouldTransitionEnter: Boolean = false, shouldTransitionExit: Boolean = true, transitionTarget: Boolean = true, transitionDurationMs: Int = 360, pullExitMiniTranslateY: Dp = 72.dp, panEdgeProtection: Rect = Rect( 0f, 0f, with(LocalDensity.current) { containerWidth.toPx() }, with(LocalDensity.current) { containerHeight.toPx() }), maxScale: Float = 4f, onPress: suspend PressGestureScope.(Offset) -> Unit = { }, onBeginPullExit: () -> Boolean, onLongPress: (() -> Unit)? = null, onTapExit: (afterTransition: Boolean) -> Unit, content: @Composable (transition: Transition, scale: Float, rect: Rect, onImageRatioEnsured: (Float) -> Unit) -> Unit ) { val (imageWidth, imageHeight) = calculateImageSize(containerWidth, containerHeight, imageRatio, isLongImage) var calculatedImageRatio by remember { mutableStateOf(imageRatio) } val density = LocalDensity.current val imagePaddingFix by remember(density, panEdgeProtection, isLongImage, containerWidth, containerHeight, calculatedImageRatio, imageRatio) { val (expectWidth, expectHeight) = calculateImageSize(containerWidth, containerHeight, calculatedImageRatio, isLongImage) val widthPadding = with(density) { (imageWidth - expectWidth).toPx() / 2 } val heightPadding = with(density) { (imageHeight - expectHeight).toPx() / 2 } mutableStateOf(widthPadding to heightPadding) } val usedImageRatioUpdater = remember { val func: (Float) -> Unit = { value -> if (value > 0) { calculatedImageRatio = value } } func } var backgroundTargetAlpha by remember { mutableStateOf(1f) } val photoTargetNormalTranslateX = with(LocalDensity.current) { ((containerWidth - imageWidth) / 2f).toPx() } val photoTargetNormalTranslateY = with(LocalDensity.current) { ((containerHeight - imageHeight) / 2f).toPx() } var photoTargetScale by remember(containerWidth, containerHeight) { mutableStateOf(1f) } var photoTargetTranslateX by remember(containerWidth, containerHeight) { mutableStateOf(photoTargetNormalTranslateX) } var photoTargetTranslateY by remember(containerWidth, containerHeight) { mutableStateOf(photoTargetNormalTranslateY) } val containerWidthPx = with(LocalDensity.current) { containerWidth.toPx() } val containerHeightPx = with(LocalDensity.current) { containerHeight.toPx() } val imageWidthPx = with(LocalDensity.current) { imageWidth.toPx() } val imageHeightPx = with(LocalDensity.current) { imageHeight.toPx() } var isGestureHandling by remember(containerWidth, containerHeight) { mutableStateOf(false) } var transitionTargetState by remember(containerWidth, containerHeight, transitionTarget) { mutableStateOf(transitionTarget) } val transitionState = remember(containerWidth, containerHeight) { MutableTransitionState(!shouldTransitionEnter) } val scaleHandler: (Offset, Float, Boolean) -> Unit = remember(containerWidth, containerHeight, maxScale, imageRatio) { lambda@{ center, scaleParam, edgeProtection -> var scale = scaleParam if (photoTargetScale * scaleParam > maxScale) { scale = maxScale / photoTargetScale } if (scale == 1f) { return@lambda } var targetLeft = center.x + ((photoTargetTranslateX - center.x) * scale) var targetTop = center.y + ((photoTargetTranslateY - center.y) * scale) val targetWidth = imageWidthPx * photoTargetScale * scale val targetHeight = imageHeightPx * photoTargetScale * scale if (edgeProtection) { when { containerWidthPx > targetWidth -> { targetLeft = (containerWidthPx - targetWidth) / 2 } targetLeft > 0 -> { targetLeft = 0f } targetLeft + targetWidth < containerWidthPx -> { targetLeft = containerWidthPx - targetWidth } } when { containerHeightPx > targetHeight -> { targetTop = (containerHeightPx - targetHeight) / 2 } targetTop > 0 -> { targetTop = 0f } targetTop + targetHeight < containerHeightPx -> { targetTop = containerHeightPx - targetHeight } } } photoTargetTranslateX = targetLeft photoTargetTranslateY = targetTop photoTargetScale *= scale } } val reset: () -> Unit = remember(containerWidth, containerHeight, imageRatio) { { backgroundTargetAlpha = 1f photoTargetScale = 1f photoTargetTranslateX = photoTargetNormalTranslateX photoTargetTranslateY = photoTargetNormalTranslateY } } transitionState.targetState = transitionTargetState val transition = updateTransition(transitionState = transitionState, label = "PhotoPager") val nestedScrollConnection = remember { GestureNestScrollConnection() } Box( modifier = Modifier .width(containerWidth) .height(containerHeight) ) { PhotoBackgroundWithTransition(backgroundTargetAlpha, transition, transitionDurationMs) { PhotoBackground(alpha = it) } Box( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) .pointerInput(containerWidth, containerHeight, maxScale, shouldTransitionExit, onTapExit, onBeginPullExit, imagePaddingFix) { coroutineScope { launch { detectTapGestures( onTap = { if (shouldTransitionExit) { transitionTargetState = false } else { onTapExit(false) } }, onLongPress = { onLongPress?.invoke() }, onDoubleTap = { if (photoTargetScale == 1f) { var scale = 2f val alignScale = (containerWidth / imageWidth).coerceAtLeast((containerHeight / imageHeight)) if (alignScale > 1.25 && alignScale < scale) { scale = alignScale } scaleHandler.invoke(it, scale, true) } else { reset() } }, onPress = onPress ) } launch { forEachGesture { awaitPointerEventScope { var zoom = 1f var pan = Offset.Zero val touchSlop = viewConfiguration.touchSlop var isZooming = false var isPanning = false var isExitPanning = false isGestureHandling = false awaitFirstDown(requireUnconsumed = false) nestedScrollConnection.canConsumeEvent = false nestedScrollConnection.isIntercepted = false do { val event = awaitPointerEvent() if (isZooming || isExitPanning) { nestedScrollConnection.isIntercepted = true } val needHandle = nestedScrollConnection.canConsumeEvent || event.changes.none { it.positionChangeConsumed() } if (needHandle) { val zoomChange = event.calculateZoom() val panChange = event.calculatePan() if (!isZooming && !isPanning) { zoom *= zoomChange pan += panChange val centroidSize = event.calculateCentroidSize(useCurrent = false) val zoomMotion = abs(1 - zoom) * centroidSize val panMotion = pan.getDistance() if (zoomMotion > touchSlop) { isGestureHandling = true isZooming = true } else if (panMotion > touchSlop) { isPanning = true isGestureHandling = true } } if (isZooming) { val centroid = event.calculateCentroid(useCurrent = false) if (zoomChange != 1f) { scaleHandler(centroid, zoomChange, true) } event.changes.forEach { if (it.positionChanged()) { it.consumeAllChanges() } } } else if (isPanning) { if (!isExitPanning) { var xConsumed = false var yConsumed = false if (panChange != Offset.Zero) { if (panChange.x > 0) { val fixEdgeLeft = panEdgeProtection.left - imagePaddingFix.first * photoTargetScale if (photoTargetTranslateX < fixEdgeLeft) { photoTargetTranslateX = (photoTargetTranslateX + panChange.x).coerceAtMost(fixEdgeLeft) xConsumed = true } } if (panChange.x < 0) { val w = imageWidthPx * photoTargetScale val fixEdgeRight = panEdgeProtection.right + imagePaddingFix.first * photoTargetScale if (photoTargetTranslateX + w > fixEdgeRight) { photoTargetTranslateX = (photoTargetTranslateX + panChange.x).coerceAtLeast(fixEdgeRight - w) xConsumed = true } } if (panChange.y > 0) { val fixEdgeTop = panEdgeProtection.top - imagePaddingFix.second * photoTargetScale if (photoTargetTranslateY < fixEdgeTop) { photoTargetTranslateY = (photoTargetTranslateY + panChange.y).coerceAtMost(fixEdgeTop) yConsumed = true } else if (!xConsumed && panChange.y > panChange.x.absoluteValue) { isExitPanning = photoTargetScale == 1f && onBeginPullExit() } } if (panChange.y < 0) { val h = imageHeightPx * photoTargetScale val fixEgeBottom = panEdgeProtection.bottom + imagePaddingFix.second * photoTargetScale if (photoTargetTranslateY + h > fixEgeBottom) { photoTargetTranslateY = (photoTargetTranslateY + panChange.y).coerceAtLeast(fixEgeBottom - h) yConsumed = true } } } if (xConsumed || yConsumed) { event.changes.forEach { if (it.positionChanged()) { it.consumeAllChanges() } } } } if (isExitPanning) { val center = event.calculateCentroid(useCurrent = true) val scaleChange = 1 - panChange.y / containerHeightPx / 2 val finalScale = (photoTargetScale * scaleChange) .coerceAtLeast(0.5f) .coerceAtMost(1f) backgroundTargetAlpha = finalScale photoTargetTranslateX += panChange.x photoTargetTranslateY += panChange.y scaleHandler(center, finalScale / photoTargetScale, false) event.changes.forEach { if (it.positionChanged()) { it.consumeAllChanges() } } } } } } while (event.changes.any { it.pressed }) isGestureHandling = false if (isZooming) { if (photoTargetScale < 1f) { reset() } } if (isExitPanning) { if (photoTargetTranslateY - photoTargetNormalTranslateY < pullExitMiniTranslateY.toPx()) { reset() } else { transitionTargetState = false } } } } } } } ) { if (initRect == null || initRect == Rect.Zero || imageRatio <= 0f) { PhotoContentWithAlphaTransition( transition = transition, transitionDurationMs = transitionDurationMs, isGestureHandling = isGestureHandling, scale = photoTargetScale, translateX = photoTargetTranslateX, translateY = photoTargetTranslateY ) { alpha, scale, translateX, translateY -> PhotoTransformContent( alpha, imageWidthPx, imageHeightPx, scale, scale, translateX, translateY ) { val imageLeft = translateX + imagePaddingFix.first * it val imageTop = translateY + imagePaddingFix.second * it content( transition, it, Rect(imageLeft, imageTop, imageLeft + imageWidthPx * it, imageTop + imageHeightPx * it), usedImageRatioUpdater ) } } } else { PhotoContentWithRectTransition( imageWidth = imageWidthPx, imageHeight = imageHeightPx, initRect = initRect, scale = photoTargetScale, translateX = photoTargetTranslateX, translateY = photoTargetTranslateY, transition = transition, transitionDurationMs = transitionDurationMs ) { scaleX, scaleY, translateX, translateY -> PhotoTransformContent(1f, imageWidthPx, imageHeightPx, scaleX, scaleY, translateX, translateY) { val imageLeft = translateX + imagePaddingFix.first * it val imageTop = translateY + imagePaddingFix.second * it content( transition, it, Rect(imageLeft, imageTop, imageLeft + imageWidthPx * it, imageTop + imageHeightPx * it), usedImageRatioUpdater ) } } } } } if (!transitionState.currentState && !transitionState.targetState) { onTapExit(true) } } @Composable fun PhotoBackgroundWithTransition( backgroundTargetAlpha: Float, transition: Transition, transitionDurationMs: Int, content: @Composable (alpha: Float) -> Unit ) { val alpha = transition.animateFloat( transitionSpec = { tween(durationMillis = transitionDurationMs) }, label = "PhotoBackgroundWithTransition" ) { if (it) backgroundTargetAlpha else 0f } content(alpha.value) } @Composable fun PhotoContentWithAlphaTransition( transition: Transition, transitionDurationMs: Int, isGestureHandling: Boolean, scale: Float, translateX: Float, translateY: Float, content: @Composable (alpha: Float, scale: Float, translateX: Float, translateY: Float) -> Unit ) { val alphaState = transition.animateFloat( transitionSpec = { tween(durationMillis = transitionDurationMs) }, label = "PhotoContentWithAlphaTransition" ) { if (it) 1f else 0f } val duration = if (isGestureHandling) 0 else transitionDurationMs val scaleState = animateFloatAsState( targetValue = scale, animationSpec = tween(durationMillis = duration) ) val translateXState = animateFloatAsState( targetValue = translateX, animationSpec = tween(durationMillis = duration) ) val translateYState = animateFloatAsState( targetValue = translateY, animationSpec = tween(durationMillis = duration) ) content(alphaState.value, scaleState.value, translateXState.value, translateYState.value) } @Composable fun PhotoContentWithRectTransition( imageWidth: Float, imageHeight: Float, initRect: Rect, scale: Float, translateX: Float, translateY: Float, transition: Transition, transitionDurationMs: Int, content: @Composable (scaleX: Float, scaleY: Float, translateX: Float, translateY: Float) -> Unit ) { val rect = transition.animateRect( transitionSpec = { tween(durationMillis = transitionDurationMs) }, label = "PhotoContentWithRectTransition" ) { if (it) Rect(translateX, translateY, translateX + imageWidth * scale, translateY + imageHeight * scale) else initRect } content( (rect.value.width / imageWidth).coerceAtLeast(0f), (rect.value.height / imageHeight).coerceAtLeast(0f), rect.value.left, rect.value.top ) } @Composable fun PhotoBackground( alpha: Float ) { Box( modifier = Modifier .fillMaxSize() .alpha(alpha) .background(Color.Black) ) } @Composable fun PhotoTransformContent( alpha: Float, width: Float, height: Float, scaleX: Float, scaleY: Float, translateX: Float, translateY: Float, content: @Composable (scale: Float) -> Unit ) { val widthDp = with(LocalDensity.current) { width.toDp() } val heightDp = with(LocalDensity.current) { height.toDp() } val scale = scaleX.coerceAtLeast(scaleY) val clipSize = remember(scaleX, scaleY, width, height) { if(scale == 0f){ Size(0f, 0f) }else{ val expectedW = width * scaleX / scale val expectedH = height * scaleY / scale val clipW = (width - expectedW) / 2 val clipH = (height - expectedH) / 2 Size(clipW, clipH) } } Box( modifier = Modifier .width(widthDp) .height(heightDp) .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.alpha = alpha this.scaleX = scale this.scaleY = scale this.clip = true this.shape = object : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density) = Outline.Rectangle(Rect(clipSize.width, clipSize.height, size.width - clipSize.width, size.height - clipSize.height)) override fun toString(): String = "PhotoTransformShape" } this.translationX = translateX - clipSize.width * scale this.translationY = translateY - clipSize.height * scale } ) { content(scale) } } internal class GestureNestScrollConnection : NestedScrollConnection { var isIntercepted: Boolean = false var canConsumeEvent: Boolean = false override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (isIntercepted) { return available } return super.onPreScroll(available, source) } override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { if (available.y > 0) { canConsumeEvent = true } return available } } private fun calculateImageSize(containerWidth: Dp, containerHeight: Dp, imageRatio: Float, isLongImage: Boolean): Pair { val layoutRatio = containerWidth / containerHeight return when { isLongImage || imageRatio <= 0f -> containerWidth to containerHeight imageRatio >= layoutRatio -> containerWidth to (containerWidth / imageRatio) else -> (containerHeight * imageRatio) to containerHeight } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/Loading.kt ================================================ package com.qmuiteam.photo.compose import androidx.compose.animation.core.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun QMUIPhotoLoading( size: Dp = 32.dp, duration: Int = 600, lineCount: Int = 12, lineColor: Color = Color.LightGray, ){ val transition = rememberInfiniteTransition() val degree = 360f / lineCount val rotate = transition.animateValue( initialValue = 0, targetValue = lineCount - 1, typeConverter = Int.VectorConverter, animationSpec = infiniteRepeatable(tween(duration, 0, LinearEasing)) ) Canvas(modifier = Modifier.size(size)) { rotate(rotate.value * degree, center){ for (i in 0 until lineCount) { rotate(degree * i, center){ drawLine( lineColor.copy((i+1) / lineCount.toFloat()), center + Offset(this.size.width / 4f, 0f), center + Offset(this.size.width / 2f, 0f), this.size.width / 16f ) } } } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/PhotoClipper.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.photo.compose import android.graphics.Bitmap import android.graphics.Matrix import android.graphics.drawable.Drawable import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.withSaveLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.qmuiteam.photo.data.PhotoLoadStatus import com.qmuiteam.photo.data.QMUIPhotoProvider private class ClipperPhotoInfo( var scale: Float = 1f, var rect: Rect? = null, var drawable: Drawable? = null, var clipArea: Rect ) val DefaultClipFocusAreaSquareCenter = Rect.Zero @Composable fun QMUIPhotoClipper( photoProvider: QMUIPhotoProvider, maskColor: Color = Color.Black.copy(0.64f), clipFocusArea: Rect = DefaultClipFocusAreaSquareCenter, drawClipFocusArea: DrawScope.(Rect) -> Unit = { area -> drawCircle( Color.Black, radius = area.size.minDimension / 2, center = area.center, blendMode = BlendMode.DstOut ) }, bitmapClipper: (origin: Bitmap, clipArea: Rect, scale: Float) -> Bitmap? = { origin, clipArea, scale -> val matrix = Matrix() matrix.postScale(scale, scale) Bitmap.createBitmap( origin, clipArea.left.toInt(), clipArea.top.toInt(), clipArea.width.toInt(), clipArea.height.toInt(), matrix, false ) }, operateContent: @Composable BoxWithConstraintsScope.(doClip: () -> Bitmap?) -> Unit ) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val focusArea = if (clipFocusArea == DefaultClipFocusAreaSquareCenter) { val size = (constraints.maxWidth.coerceAtMost(constraints.maxHeight)).toFloat() val left = (constraints.maxWidth - size) / 2 val top = (constraints.maxHeight - size) / 2 Rect(left, top, left + size, top + size) } else { clipFocusArea } val photoInfo = remember(photoProvider) { ClipperPhotoInfo(clipArea = focusArea) }.apply { clipArea = focusArea } val doClip = remember(photoInfo) { val func: () -> Bitmap? = lambda@{ val origin = photoInfo.drawable?.toBitmap() ?: return@lambda null val rect = photoInfo.rect ?: return@lambda null val scale = rect.width / origin.width val clipRect = photoInfo.clipArea.translate(Offset(-rect.left, -rect.top)) val imageArea = Rect( clipRect.left / scale, clipRect.top / scale, clipRect.right / scale, clipRect.bottom / scale ) bitmapClipper(origin, imageArea, scale) } func } QMUIGesturePhoto( containerWidth = maxWidth, containerHeight = maxHeight, imageRatio = photoProvider.ratio(), isLongImage = photoProvider.isLongImage(), shouldTransitionExit = false, panEdgeProtection = focusArea, onBeginPullExit = { false }, onTapExit = {} ) { _, scale, rect, onImageRatioEnsured -> photoInfo.scale = scale photoInfo.rect = rect QMUIPhotoContent(photoProvider) { photoInfo.drawable = it if (it.intrinsicWidth > 0 && it.intrinsicHeight > 0) { onImageRatioEnsured(it.intrinsicWidth.toFloat() / it.intrinsicHeight) } } } Canvas(modifier = Modifier.fillMaxSize()) { drawContext.canvas.withSaveLayer(Rect(Offset.Zero, drawContext.size), Paint()) { drawRect(maskColor) drawClipFocusArea(focusArea) } } operateContent(doClip) } } @Composable fun BoxScope.QMUIPhotoContent( photoProvider: QMUIPhotoProvider, onSuccess: (Drawable) -> Unit ) { var loadStatus by remember { mutableStateOf(PhotoLoadStatus.loading) } val photo = remember(photoProvider) { photoProvider.photo() } photo?.Compose( contentScale = ContentScale.Fit, isContainerDimenExactly = true, onSuccess = { loadStatus = PhotoLoadStatus.success onSuccess.invoke(it.drawable) }, onError = { loadStatus = PhotoLoadStatus.failed }) if (loadStatus == PhotoLoadStatus.loading) { Box(modifier = Modifier.align(Alignment.Center)) { QMUIPhotoLoading(size = 48.dp) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/PhotoConfig.kt ================================================ package com.qmuiteam.photo.compose import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color class QMUIPhotoConfig( val blankColor: Color = Color.LightGray ) val qmuiPhotoDefaultConfig by lazy { QMUIPhotoConfig() } val QMUILocalPhotoConfig = staticCompositionLocalOf { qmuiPhotoDefaultConfig } @Composable fun QMUIDefaultPhotoConfigProvider(content: @Composable () -> Unit) { CompositionLocalProvider(QMUILocalPhotoConfig provides qmuiPhotoDefaultConfig) { content() } } @Composable fun BlankBox() { val blankColor = QMUILocalPhotoConfig.current.blankColor if (blankColor != Color.Transparent) { Box( modifier = Modifier .fillMaxSize() .background(blankColor) ) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt ================================================ package com.qmuiteam.photo.compose import androidx.activity.ComponentActivity 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.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.qmuiteam.photo.activity.QMUIPhotoViewerActivity import com.qmuiteam.photo.data.* import com.qmuiteam.photo.util.getWindowSize import kotlinx.coroutines.launch const val SINGLE_HIGH_IMAGE_MINI_SCREEN_HEIGHT_RATIO = -1F class QMUIPhotoThumbnailConfig( val singleSquireImageWidthRatio: Float = 0.5f, val singleWideImageMaxWidthRatio: Float = 0.667f, val singleHighImageDefaultWidthRatio: Float = 0.5f, val singleHighImageMiniHeightRatio: Float = SINGLE_HIGH_IMAGE_MINI_SCREEN_HEIGHT_RATIO, val singleLongImageWidthRatio: Float = 0.5f, val averageIfTwoImage: Boolean = true, val horGap: Dp = 5.dp, val verGap: Dp = 5.dp, val alphaWhenPressed: Float = 1f ) val qmuiDefaultPhotoThumbnailConfig = QMUIPhotoThumbnailConfig() @Composable private fun QMUIPhotoThumbnailItem( thumb: QMUIPhoto?, width: Dp, height: Dp, alphaWhenPressed: Float, isContainerDimenExactly: Boolean, onLayout: (offset: Offset, size: IntSize) -> Unit, onPhotoLoaded: (PhotoResult) -> Unit, click: (() -> Unit)?, ) { val interactionSource = remember { MutableInteractionSource() } val isPressed = interactionSource.collectIsPressedAsState() Box(modifier = Modifier .width(width) .height(height) .let { if (click != null) { it .clickable(interactionSource, null) { click.invoke() } .alpha(if (isPressed.value) alphaWhenPressed else 1f) } else { it } } .onGloballyPositioned { onLayout(it.positionInWindow(), it.size) } ) { thumb?.Compose( contentScale = if (isContainerDimenExactly) ContentScale.Crop else ContentScale.Fit, isContainerDimenExactly = isContainerDimenExactly, onSuccess = { onPhotoLoaded(it) }, onError = null ) } } @Composable fun QMUIPhotoThumbnailWithViewer( targetActivity: Class = QMUIPhotoViewerActivity::class.java, activity: ComponentActivity, images: List, config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig } ) { QMUIPhotoThumbnail(images, config) { list, index -> val intent = QMUIPhotoViewerActivity.intentOf(activity, targetActivity, list, index) activity.startActivity(intent) activity.overridePendingTransition(0, 0) } } @Composable fun QMUIPhotoThumbnail( images: List, config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig }, onClick: ((images: List, index: Int) -> Unit)? = null ) { if (images.size < 0) { return } val renderInfo = remember(images) { Array(images.size) { QMUIPhotoTransitionInfo(images[it], null, null, null) } } val context = LocalContext.current BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { if (images.size == 1) { val image = images[0] val thumb = remember(image) { image.thumbnail(true) } if (thumb != null) { val ratio = image.ratio() when { ratio <= 0 -> { QMUIPhotoThumbnailItem( thumb, Dp.Unspecified, Dp.Unspecified, config.alphaWhenPressed, isContainerDimenExactly = false, onLayout = { offset, size -> renderInfo[0].offsetInWindow = offset renderInfo[0].size = size }, onPhotoLoaded = { renderInfo[0].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), 0) } } else null ) } ratio == 1f -> { val wh = maxWidth * config.singleSquireImageWidthRatio QMUIPhotoThumbnailItem( thumb, wh, wh, config.alphaWhenPressed, isContainerDimenExactly = true, onLayout = { offset, size -> renderInfo[0].offsetInWindow = offset renderInfo[0].size = size }, onPhotoLoaded = { renderInfo[0].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), 0) } } else null ) } ratio > 1f -> { val width = maxWidth * config.singleWideImageMaxWidthRatio val height = width / ratio QMUIPhotoThumbnailItem( thumb, width, height, config.alphaWhenPressed, isContainerDimenExactly = true, onLayout = { offset, size -> renderInfo[0].offsetInWindow = offset renderInfo[0].size = size }, onPhotoLoaded = { renderInfo[0].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), 0) } } else null ) } image.isLongImage() -> { val width = maxWidth * config.singleLongImageWidthRatio val heightRatio = if (config.singleHighImageMiniHeightRatio == SINGLE_HIGH_IMAGE_MINI_SCREEN_HEIGHT_RATIO) { val windowSize = getWindowSize(context) windowSize.width * 1f / windowSize.height } else { config.singleHighImageMiniHeightRatio } val height = width / heightRatio QMUIPhotoThumbnailItem( thumb, width, height, config.alphaWhenPressed, isContainerDimenExactly = true, onLayout = { offset, size -> renderInfo[0].offsetInWindow = offset renderInfo[0].size = size }, onPhotoLoaded = { renderInfo[0].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), 0) } } else null ) } else -> { var width = maxWidth * config.singleHighImageDefaultWidthRatio var height = width / ratio val heightMiniRatio = if (config.singleHighImageMiniHeightRatio == SINGLE_HIGH_IMAGE_MINI_SCREEN_HEIGHT_RATIO) { val windowSize = getWindowSize(context) windowSize.width * 1f / windowSize.height } else { config.singleHighImageMiniHeightRatio } if (ratio < heightMiniRatio) { height = width * heightMiniRatio width = height * ratio } QMUIPhotoThumbnailItem( thumb, width, height, config.alphaWhenPressed, isContainerDimenExactly = true, onLayout = { offset, size -> renderInfo[0].offsetInWindow = offset renderInfo[0].size = size }, onPhotoLoaded = { renderInfo[0].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), 0) } } else null ) } } } } else if (images.size == 2 && config.averageIfTwoImage) { RowImages(images, renderInfo, config, maxWidth, 2, 0, onClick) } else { Column(modifier = Modifier.fillMaxWidth()) { for (i in 0 until (images.size / 3 + if (images.size % 3 > 0) 1 else 0).coerceAtMost( 3 )) { if (i > 0) { Spacer(modifier = Modifier.height(config.verGap)) } RowImages( images, renderInfo, config, this@BoxWithConstraints.maxWidth, 3, i * 3, onClick ) } } } } } @Composable fun RowImages( images: List, renderInfo: Array, config: QMUIPhotoThumbnailConfig, containerWidth: Dp, rowCount: Int, startIndex: Int, onClick: ((images: List, index: Int) -> Unit)? ) { val wh = (containerWidth - config.horGap * (rowCount - 1)) / rowCount Row( modifier = Modifier .fillMaxWidth() .height(wh) ) { for (i in startIndex until (startIndex + rowCount).coerceAtMost(images.size)) { if (i != startIndex) { Spacer(modifier = Modifier.width(config.horGap)) } val image = images[i] QMUIPhotoThumbnailItem( remember(image) { image.thumbnail(true) }, wh, wh, config.alphaWhenPressed, isContainerDimenExactly = true, onLayout = { offset, size -> renderInfo[i].offsetInWindow = offset renderInfo[i].size = size }, onPhotoLoaded = { renderInfo[i].photo = it.drawable }, click = if (onClick != null) { { onClick.invoke(renderInfo.toList(), i) } } else null ) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Buckets.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.animation.* 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.items 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.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.* import androidx.core.view.WindowInsetsCompat import com.qmuiteam.compose.core.ex.drawBottomSeparator import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets import com.qmuiteam.compose.core.provider.dp import com.qmuiteam.compose.core.ui.QMUIMarkIcon import com.qmuiteam.photo.data.QMUIMediaPhotoBucketVO @Composable fun ConstraintLayoutScope.QMUIPhotoBucketChooser( focus: Boolean, data: List, currentId: String, onBucketClick: (QMUIMediaPhotoBucketVO) -> Unit, onDismiss: () -> Unit ) { val (mask, content) = createRefs() AnimatedVisibility( visible = focus, modifier = Modifier.constrainAs(mask) { width = Dimension.fillToConstraints height = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, enter = fadeIn(), exit = fadeOut() ) { Box( modifier = Modifier .fillMaxSize() .background(QMUILocalPickerConfig.current.bucketChooserMaskColor) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onDismiss() } ) } AnimatedVisibility( visible = focus, modifier = Modifier.constrainAs(content) { width = Dimension.fillToConstraints height = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, enter = slideInVertically(initialOffsetY = { -it }), exit = slideOutVertically(targetOffsetY = { -it }) ) { BoxWithConstraints() { val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.navigationBars() ).dp() LazyColumn( modifier = Modifier .fillMaxWidth() .heightIn(max = (maxHeight - insets.bottom) * 0.8f) .wrapContentHeight() .background(QMUILocalPickerConfig.current.bucketChooserBgColor), ) { items(data, key = { it.id }) { QMUIPhotoBucketItem(it, it.id == currentId, onBucketClick) } } } } } @Composable fun QMUIPhotoBucketItem( data: QMUIMediaPhotoBucketVO, isCurrent: Boolean, onBucketClick: (QMUIMediaPhotoBucketVO) -> Unit ) { val h = 60.dp val textBeginMargin = 16.dp val config = QMUILocalPickerConfig.current ConstraintLayout(modifier = Modifier .fillMaxWidth() .height(h) .drawBehind { drawBottomSeparator(insetStart = h + textBeginMargin, color = config.commonSeparatorColor) } .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(color = config.bucketChooserIndicationColor) ) { onBucketClick(data) } ) { val (pic, title, num, mark) = createRefs() val chainHor = createHorizontalChain(title, num, chainStyle = ChainStyle.Packed(0f)) constrain(chainHor) { start.linkTo(pic.end, margin = textBeginMargin) end.linkTo(mark.start, margin = 16.dp) } Box(modifier = Modifier .size(h) .constrainAs(pic) { start.linkTo(parent.start) top.linkTo(parent.top) bottom.linkTo(parent.bottom) }) { val thumbnail = remember(data) { data.list.firstOrNull()?.photoProvider?.thumbnail(true) } thumbnail?.Compose( contentScale = ContentScale.Crop, isContainerDimenExactly = true, onSuccess = null, onError = null ) } Text( text = data.name, fontSize = 17.sp, color = config.bucketChooserMainTextColor, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.constrainAs(title) { width = Dimension.preferredWrapContent top.linkTo(parent.top) bottom.linkTo(parent.bottom) } ) Text( text = "(${data.list.size})", fontSize = 17.sp, color = config.bucketChooserCountTextColor, modifier = Modifier.constrainAs(num) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) } ) QMUIMarkIcon( modifier = Modifier.constrainAs(mark) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) end.linkTo(parent.end, 16.dp) visibility = if (isCurrent) Visibility.Visible else Visibility.Gone }, tint = config.commonIconCheckedTintColor ) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Common.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.animation.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image 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.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.graphics.ColorFilter import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.qmuiteam.compose.core.ui.CheckStatus import com.qmuiteam.compose.core.ui.PressWithAlphaBox import com.qmuiteam.compose.core.ui.QMUICheckBox import kotlinx.coroutines.flow.StateFlow @OptIn(ExperimentalAnimationApi::class) @Composable fun QMUIPhotoPickCheckBox(pickIndex: Int) { val config = QMUILocalPickerConfig.current val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() } AnimatedVisibility( visible = pickIndex < 0, enter = fadeIn(), exit = fadeOut() ) { Canvas(modifier = Modifier.fillMaxSize()) { drawCircle( color = config.commonIconNormalTintColor, radius = (size.minDimension - strokeWidth) / 2.0f, style = Stroke(strokeWidth) ) } } AnimatedVisibility( visible = pickIndex >= 0, enter = fadeIn(), exit = fadeOut() ) { Box( modifier = Modifier .fillMaxSize() .clip(CircleShape) .background(config.commonIconCheckedTintColor), contentAlignment = Alignment.Center ) { if (transition.targetState != EnterExitState.PostExit) { Text( text = "${pickIndex + 1}", color = config.commonIconCheckedTextColor, fontSize = 12.sp ) } } } } @Composable fun QMUIPhotoPickRadio( checked: Boolean, ratioSize: Dp = 18.dp, strokeWidthDp: Dp = 1.6.dp ) { Box(modifier = Modifier.size(ratioSize)) { val strokeWidth = with(LocalDensity.current) { strokeWidthDp.toPx() } val config = QMUILocalPickerConfig.current AnimatedVisibility( visible = !checked, enter = fadeIn(), exit = fadeOut() ) { Canvas(modifier = Modifier.size(ratioSize)) { drawCircle( color = config.commonIconNormalTintColor, radius = (size.minDimension - strokeWidth) / 2.0f, style = Stroke(strokeWidth) ) } } AnimatedVisibility( visible = checked, enter = fadeIn(), exit = fadeOut() ) { Canvas(modifier = Modifier.size(ratioSize)) { drawCircle( color = config.commonIconCheckedTintColor, radius = (size.minDimension - strokeWidth) / 2.0f, style = Stroke(strokeWidth) ) drawCircle( color = config.commonIconCheckedTintColor, radius = (size.minDimension - strokeWidth * 4) / 2.0f, ) } } } } @Composable fun OriginOpenButton( modifier: Modifier = Modifier, isOriginOpenFlow: StateFlow, onToggleOrigin: (toOpen: Boolean) -> Unit, ) { val isOriginOpen by isOriginOpenFlow.collectAsState() Row( modifier = modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onToggleOrigin.invoke(!isOriginOpen) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Absolute.spacedBy(5.dp) ) { QMUIPhotoPickRadio(isOriginOpen) Text( "原图", fontSize = 17.sp, color = QMUILocalPickerConfig.current.commonTextButtonTextColor ) } } @Composable fun PickCurrentCheckButton( modifier: Modifier = Modifier, isPicked: Boolean, onPicked: (toPick: Boolean) -> Unit, ) { val config = QMUILocalPickerConfig.current Row( modifier = modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onPicked.invoke(!isPicked) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Absolute.spacedBy(5.dp) ) { QMUICheckBox( size = 18.dp, status = if (isPicked) CheckStatus.checked else CheckStatus.none, tint = if (isPicked) config.commonIconCheckedTintColor else config.commonIconNormalTintColor, background = if (isPicked) config.commonIconNormalTintColor else Color.Transparent, ) Text( "选择", fontSize = 17.sp, color = QMUILocalPickerConfig.current.commonTextButtonTextColor ) } } @Composable internal fun CommonTextButton( modifier: Modifier, enable: Boolean, text: String, onClick: () -> Unit ) { PressWithAlphaBox( enable = enable, modifier = Modifier .fillMaxHeight() .padding(horizontal = 16.dp) .then(modifier), onClick = { onClick() } ) { Text( text, fontSize = 17.sp, color = QMUILocalPickerConfig.current.commonTextButtonTextColor, modifier = Modifier.align(Alignment.Center) ) } } @Composable internal fun CommonImageButton( modifier: Modifier = Modifier, res: Int, enabled: Boolean = true, checked: Boolean = false, onClick: () -> Unit ){ PressWithAlphaBox( modifier = modifier, enable = enabled, onClick = { onClick() } ) { val config = QMUILocalPickerConfig.current Image( painter = painterResource(res), contentDescription = "", colorFilter = ColorFilter.tint(if(checked) config.commonIconCheckedTintColor else config.commonIconNormalTintColor), contentScale = ContentScale.Inside ) } } @Composable internal fun CommonButton( modifier: Modifier = Modifier, enabled: Boolean, text: String, onClick: () -> Unit ) { val config = QMUILocalPickerConfig.current val interactionSource = remember { MutableInteractionSource() } val isPressed = interactionSource.collectIsPressedAsState() val bgColor = when { !enabled -> config.commonButtonDisableBgColor isPressed.value -> config.commonButtonPressBgColor else -> config.commonButtonNormalBgColor } val textColor = when { !enabled -> config.commonButtonDisabledTextColor isPressed.value -> config.commonButtonPressedTextColor else -> config.commonButtonNormalTextColor } Box( modifier = modifier .clip(RoundedCornerShape(4.dp)) .background(bgColor) .clickable( interactionSource = interactionSource, indication = null, enabled = enabled ) { onClick() } .padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 4.dp) ) { Text( text = text, fontSize = 17.sp, color = textColor ) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Config.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf 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.QMUITopBarItem import com.qmuiteam.compose.core.ui.qmuiPrimaryColor import kotlinx.coroutines.flow.StateFlow class QMUIPhotoPickerConfig( val editable: Boolean = true, val primaryColor: Color = qmuiPrimaryColor, val commonTextButtonTextColor: Color = Color.White, val commonSeparatorColor: Color = Color.White.copy(alpha = 0.3f), val commonIconNormalTintColor: Color = Color.White.copy(0.9f), val commonIconCheckedTintColor: Color = primaryColor, val commonIconCheckedTextColor: Color = Color.White.copy(alpha = 0.6f), val commonButtonNormalTextColor: Color = Color.White, val commonButtonNormalBgColor: Color = primaryColor, val commonButtonDisabledTextColor: Color = Color.White.copy(alpha = 0.3f), val commonButtonDisableBgColor: Color = Color.White.copy(alpha = 0.15f), val commonButtonPressBgColor: Color = primaryColor.copy(alpha = 0.8f), val commonButtonPressedTextColor: Color = commonButtonNormalTextColor, val topBarBgColor: Color = Color(0xFF222222), val toolBarBgColor: Color = topBarBgColor, val topBarBucketFactory: ( textFlow: StateFlow, isFocusFlow: StateFlow, onClick: () -> Unit ) -> QMUITopBarItem = { textFlow, isFocusFlow, onClick -> QMUIPhotoPickerBucketTopBarItem( bgColor = Color.White.copy(alpha = 0.15f), textColor = Color.White, iconBgColor = Color.White.copy(alpha = 0.72f), iconColor = Color(0xFF333333), textFlow = textFlow, isFocusFlow = isFocusFlow, onClick = onClick ) }, val topBarSendFactory: ( canSendSelf: Boolean, maxSelectCount: Int, selectCountFlow: StateFlow, onClick: () -> Unit ) -> QMUITopBarItem = { canSendSelf, maxSelectCount, selectCountFlow, onClick -> QMUIPhotoSendTopBarItem( text = "发送", canSendSelf = canSendSelf, maxSelectCount = maxSelectCount, selectCountFlow = selectCountFlow, onClick = onClick ) }, val screenBgColor: Color = Color(0xFF333333), val loadingColor: Color = Color.White, val tipTextColor: Color = Color.White, val gridPreferredSize: Dp = 80.dp, val gridGap: Dp = 2.dp, val gridBorderColor: Color = Color.White.copy(alpha = 0.15f), val bucketChooserMaskColor: Color = Color.Black.copy(alpha = 0.36f), val bucketChooserBgColor: Color = topBarBgColor, val bucketChooserIndicationColor: Color = Color.White.copy(alpha = 0.2f), val bucketChooserMainTextColor: Color = Color.White, val bucketChooserCountTextColor: Color = Color.White.copy(alpha = 0.64f), val editPaintOptions: List = listOf( MosaicEditPaint(16), MosaicEditPaint(50), ColorEditPaint(Color.White), ColorEditPaint(Color.Black), ColorEditPaint(Color.Red), ColorEditPaint(Color.Yellow), ColorEditPaint(Color.Green), ColorEditPaint(Color.Blue), ColorEditPaint(Color.Magenta) ), val graffitiPaintStrokeWidth: Dp = 5.dp, val mosaicPaintStrokeWidth: Dp = 20.dp, val textEditMaskColor:Color = Color.Black.copy(0.5f), val textEditColorOptions: List = listOf( ColorEditPaint(Color.White), ColorEditPaint(Color.Black), ColorEditPaint(Color.Red), ColorEditPaint(Color.Yellow), ColorEditPaint(Color.Green), ColorEditPaint(Color.Blue), ColorEditPaint(Color.Magenta) ), val textEditFontSize: TextUnit = 30.sp, val textEditLineSpace: TextUnit = 3.sp, val textCursorColor: Color = primaryColor, val editLayerDeleteAreaNormalBgColor: Color = Color.Black.copy(alpha = 0.3f), val editLayerDeleteAreaNormalFocusColor: Color = Color.Red.copy(alpha = 0.6f), ) val qmuiPhotoPickerDefaultConfig by lazy { QMUIPhotoPickerConfig() } val QMUILocalPickerConfig = staticCompositionLocalOf { qmuiPhotoPickerDefaultConfig } @Composable fun QMUIDefaultPickerConfigProvider(content: @Composable () -> Unit) { CompositionLocalProvider(QMUILocalPickerConfig provides qmuiPhotoPickerDefaultConfig) { content() } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Edit.kt ================================================ package com.qmuiteam.photo.compose.picker import android.graphics.drawable.Drawable import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedDispatcher import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.consumeDownChange import androidx.compose.ui.input.pointer.consumePositionChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility import androidx.core.graphics.drawable.toBitmap 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 import com.qmuiteam.photo.compose.QMUIGesturePhoto import com.qmuiteam.photo.data.QMUIMediaPhotoVO import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow private sealed class PickerEditScene private object PickerEditSceneNormal : PickerEditScene() private object PickerEditScenePaint : PickerEditScene() private class PickerEditSceneText(val editLayer: TextEditLayer? = null) : PickerEditScene() private class PickerEditSceneClip(val area: Rect) : PickerEditScene() private class EditSceneHolder(var scene: T? = null) private class MutablePickerPhotoInfo( var drawable: Drawable?, var mosaicBitmapCache: MutableMap = mutableMapOf() ) internal data class PickerPhotoLayoutInfo(val scale: Float, val rect: Rect) @Composable fun QMUIPhotoPickerEdit( onBackPressedDispatcher: OnBackPressedDispatcher, data: QMUIMediaPhotoVO, onBack: () -> Unit, ) { val sceneState = remember(data) { mutableStateOf(PickerEditSceneNormal) } val scene = sceneState.value val photoInfo = remember(data) { MutablePickerPhotoInfo(null) } var photoLayoutInfo by remember(data) { mutableStateOf(PickerPhotoLayoutInfo(1f, Rect.Zero)) } val paintEditLayers = remember(data) { mutableStateListOf() } val textEditLayers = remember(data) { mutableStateListOf() } val config = QMUILocalPickerConfig.current var forceHideTools by remember { mutableStateOf(false) } BoxWithConstraints(modifier = Modifier.fillMaxSize()) { QMUIGesturePhoto( containerWidth = maxWidth, containerHeight = maxHeight, imageRatio = data.model.ratio(), shouldTransitionEnter = false, shouldTransitionExit = false, isLongImage = data.photoProvider.isLongImage(), onBeginPullExit = { false }, onTapExit = { }, onPress = { textEditLayers.forEach { it.isFocusFlow.value = false } } ) { _, scale, rect, onImageRatioEnsured -> photoLayoutInfo = PickerPhotoLayoutInfo(scale, rect) QMUIPhotoPickerEditPhotoContent(data) { photoInfo.drawable = it onImageRatioEnsured(it.intrinsicWidth.toFloat() / it.intrinsicHeight) } } QMUIPhotoEditHistoryList( photoLayoutInfo, paintEditLayers, textEditLayers, onFocusLayer = { focusLayer -> textEditLayers.forEach { if (it != focusLayer) { it.isFocusFlow.value = false } } }, onEditTextLayer = { sceneState.value = PickerEditSceneText(it) }, onDeleteTextLayer = { textEditLayers.remove(it) }, onToggleDragging = { forceHideTools = it } ) AnimatedVisibility( visible = scene == PickerEditSceneNormal || scene == PickerEditScenePaint, enter = fadeIn(), exit = fadeOut() ) { QMUIPhotoPickerEditPaintScreen( paintState = scene == PickerEditScenePaint, photoInfo = photoInfo, editLayers = paintEditLayers, layoutInfo = photoLayoutInfo, forceHideTools = forceHideTools, onBack = onBack, onPaintClick = { sceneState.value = if (it) PickerEditScenePaint else PickerEditSceneNormal }, onTextClick = { sceneState.value = PickerEditSceneText() }, onClipClick = { sceneState.value = PickerEditSceneClip(Rect(Offset.Zero, photoLayoutInfo.rect.size)) }, onFinishPaintLayer = { paintEditLayers.add(it) }, onEnsureClick = { }, onRevoke = { paintEditLayers.removeLastOrNull() } ) } AnimatedVisibility( visible = scene is PickerEditSceneText, enter = fadeIn(), exit = fadeOut() ) { // For exit animation val sceneHolder = remember { EditSceneHolder(scene as? PickerEditSceneText) } if (scene is PickerEditSceneText) { sceneHolder.scene = scene } val textScene = sceneHolder.scene if (textScene != null) { QMUIPhotoPickerEditTextScreen( onBackPressedDispatcher, photoLayoutInfo, constraints, textScene.editLayer, textScene.editLayer?.color ?: config.textEditColorOptions[0].color, textScene.editLayer?.reverse ?: false, onCancel = { sceneState.value = PickerEditSceneNormal }, onFinishTextLayer = { toReplace, target -> if (toReplace != null) { val index = textEditLayers.indexOf(toReplace) if (index >= 0) { textEditLayers[index] = target } else { textEditLayers.add(target) } } else { textEditLayers.add(target) } sceneState.value = PickerEditSceneNormal } ) } } } } @Composable private fun QMUIPhotoPickerEditPaintScreen( paintState: Boolean, editLayers: List, photoInfo: MutablePickerPhotoInfo, layoutInfo: PickerPhotoLayoutInfo, forceHideTools: Boolean, onBack: () -> Unit, onPaintClick: (toPaint: Boolean) -> Unit, onTextClick: () -> Unit, onClipClick: () -> Unit, onFinishPaintLayer: (PaintEditLayer) -> Unit, onEnsureClick: () -> Unit, onRevoke: () -> Unit ) { val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout() ).dp() val paintEditOptions = QMUILocalPickerConfig.current.editPaintOptions var paintEditCurrentIndex by remember { mutableStateOf(4) } if (paintEditCurrentIndex >= paintEditOptions.size) { paintEditCurrentIndex = paintEditOptions.size - 1 } var showTools by remember { mutableStateOf(true) } ConstraintLayout(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier .fillMaxSize() .constrainAs(createRef()) { width = Dimension.fillToConstraints height = Dimension.fillToConstraints visibility = if (paintState) Visibility.Visible else Visibility.Gone start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) }) { QMUIPhotoPaintCanvas( paintEditOptions[paintEditCurrentIndex], photoInfo, layoutInfo, editLayers, onTouchBegin = { showTools = false }, onTouchEnd = { showTools = true onFinishPaintLayer(it) } ) } AnimatedVisibility( visible = showTools && !forceHideTools, modifier = Modifier.constrainAs(createRef()) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(parent.top) }, enter = fadeIn(), exit = fadeOut() ) { Box( modifier = Modifier .fillMaxWidth() .height(insets.top + 60.dp) .background(brush = Brush.verticalGradient(listOf(Color.Black.copy(0.2f), Color.Transparent))) ) } AnimatedVisibility( visible = showTools && !forceHideTools, modifier = Modifier.constrainAs(createRef()) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(parent.bottom) }, enter = fadeIn(), exit = fadeOut() ) { Box( modifier = Modifier .fillMaxWidth() .height(insets.bottom + 150.dp) .background(brush = Brush.verticalGradient(listOf(Color.Transparent, Color.Black.copy(0.2f)))) ) } AnimatedVisibility( visible = showTools && !forceHideTools, modifier = Modifier.constrainAs(createRef()) { start.linkTo(parent.start) top.linkTo(parent.top) }, enter = fadeIn(), exit = fadeOut() ) { CommonImageButton( modifier = Modifier .padding(start = insets.left + 16.dp, top = insets.top + 16.dp, end = 16.dp, bottom = 16.dp), res = R.drawable.ic_qmui_topbar_back ) { onBack() } } val (toolBar, paintChooser) = createRefs() AnimatedVisibility( visible = showTools && !forceHideTools, modifier = Modifier.constrainAs(toolBar) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(parent.bottom) }, enter = fadeIn(), exit = fadeOut() ) { QMUIPhotoPickerEditToolBar( modifier = Modifier.padding(bottom = insets.bottom, start = insets.left, end = insets.right), isPaintState = paintState, onPaintClick = onPaintClick, onTextClick = onTextClick, onClipClick = onClipClick, onEnsureClick = onEnsureClick ) } AnimatedVisibility( visible = showTools && paintState && !forceHideTools, modifier = Modifier.constrainAs(paintChooser) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(toolBar.top, 8.dp) }, enter = fadeIn(), exit = fadeOut() ) { QMUIPhotoPickerEditPaintOptions( paintEditOptions, 24.dp, paintEditCurrentIndex ) { paintEditCurrentIndex = it } } AnimatedVisibility( visible = showTools && paintState && !forceHideTools, modifier = Modifier.constrainAs(createRef()) { end.linkTo(parent.end) bottom.linkTo(paintChooser.top) }, enter = fadeIn(), exit = fadeOut() ) { CommonImageButton( modifier = Modifier .padding(start = 16.dp, top = 16.dp, end = insets.right + 16.dp, bottom = 16.dp), res = R.drawable.ic_qmui_topbar_back ) { onRevoke() } } } } @Composable private fun QMUIPhotoPickerEditTextScreen( onBackPressedDispatcher: OnBackPressedDispatcher, photoLayoutInfo: PickerPhotoLayoutInfo, constraints: Constraints, editLayer: TextEditLayer?, color: Color, isReverse: Boolean, onCancel: () -> Unit, onFinishTextLayer: (toReplace: TextEditLayer?, target: TextEditLayer) -> Unit ) { DisposableEffect("") { val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onCancel() } } onBackPressedDispatcher.addCallback(callback) object : DisposableEffectResult { override fun dispose() { callback.remove() } } } val insets = QMUILocalWindowInsets.current.getInsets( WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout() ).dp() var input by remember(editLayer) { val text = editLayer?.text ?: "" mutableStateOf(TextFieldValue(text, TextRange(text.length))) } val config = QMUILocalPickerConfig.current var usedColor by remember(color) { mutableStateOf(color) } var usedReverse by remember(isReverse) { mutableStateOf(isReverse) } val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.requestFocus() } ConstraintLayout(modifier = Modifier .fillMaxSize() .background(config.textEditMaskColor) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { if (input.text.isNotBlank()) { if (editLayer != null) { onFinishTextLayer( editLayer, TextEditLayer( input.text, editLayer.fontSize, editLayer.center, usedColor, usedReverse, editLayer.offsetFlow, editLayer.scaleFlow, editLayer.rotationFlow ) ) } else { onFinishTextLayer( null, TextEditLayer( input.text, config.textEditFontSize, Offset( (constraints.maxWidth / 2 - photoLayoutInfo.rect.left) / photoLayoutInfo.scale, constraints.maxHeight / 2 - photoLayoutInfo.rect.top ), usedColor, usedReverse, scaleFlow = MutableStateFlow(1 / photoLayoutInfo.scale) ) ) } } else { onCancel() } } .padding(insets.left, insets.top, insets.right, insets.bottom) ) { val optionsId = createRef() QMUIPhotoPickerEditTextPaintOptions( config.textEditColorOptions, 24.dp, usedColor, isReverse = usedReverse, modifier = Modifier.constrainAs(optionsId) { width = Dimension.fillToConstraints start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(parent.bottom) }, onSelect = { usedColor = it }, onReverseClick = { usedReverse = it } ) BasicTextField( value = input, onValueChange = { input = it }, modifier = Modifier .padding(16.dp) .let { if (usedReverse && input.text.isNotBlank()) { it.background(color = usedColor, shape = RoundedCornerShape(10.dp)) } else { it } } .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 3.dp) .defaultMinSize(8.dp, 48.dp) .width(IntrinsicSize.Min) .focusRequester(focusRequester) .constrainAs(createRef()) { start.linkTo(parent.start) end.linkTo(parent.end) bottom.linkTo(optionsId.top) top.linkTo(parent.top) }, textStyle = TextStyle( color = if (usedReverse) { if (usedColor == Color.White) Color.Black else Color.White } else usedColor, fontSize = config.textEditFontSize, textAlign = TextAlign.Center, fontWeight = FontWeight.Bold ), cursorBrush = SolidColor(config.textCursorColor) ) } } @Composable fun QMUIPhotoPickerEditPhotoContent( data: QMUIMediaPhotoVO, onSuccess: (Drawable) -> Unit ) { Box(modifier = Modifier.fillMaxSize()) { val photo = remember(data) { data.photoProvider.photo() } photo?.Compose( contentScale = ContentScale.Fit, isContainerDimenExactly = true, onSuccess = { if (it.drawable.intrinsicWidth > 0 && it.drawable.intrinsicHeight > 0) { onSuccess(it.drawable) } }, onError = null ) } } @Composable private fun QMUIPhotoEditHistoryList( layoutInfo: PickerPhotoLayoutInfo, editLayers: List, textEditLayers: List, onFocusLayer: (TextEditLayer) -> Unit, onEditTextLayer: (TextEditLayer) -> Unit, onDeleteTextLayer: (TextEditLayer) -> Unit, onToggleDragging: (Boolean) -> Unit ) { if (layoutInfo.rect == Rect.Zero) { return } val (w, h) = with(LocalDensity.current) { arrayOf( layoutInfo.rect.width.toDp(), layoutInfo.rect.height.toDp() ) } Canvas(modifier = Modifier .width(w / layoutInfo.scale) .height(h / layoutInfo.scale) .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.translationX = layoutInfo.rect.left this.translationY = layoutInfo.rect.top this.scaleX = layoutInfo.scale this.scaleY = layoutInfo.scale this.clip = true }) { editLayers.forEach { with(it) { draw() } } } textEditLayers.forEach { key(it) { it.Content( layoutInfo = layoutInfo, onFocus = { onFocusLayer(it) }, onToggleDragging = { isDragging -> onToggleDragging(isDragging) }, onEdit = { onEditTextLayer(it) }) { onDeleteTextLayer(it) } } } } @Composable private fun QMUIPhotoPaintCanvas( editPaint: EditPaint, photoInfo: MutablePickerPhotoInfo, layoutInfo: PickerPhotoLayoutInfo, editLayers: List, onTouchBegin: () -> Unit, onTouchEnd: (PaintEditLayer) -> Unit ) { val drawable = photoInfo.drawable ?: return val (w, h) = with(LocalDensity.current) { arrayOf( layoutInfo.rect.width.toDp(), layoutInfo.rect.height.toDp() ) } val graffitiStrokeWidth = with(LocalDensity.current) { QMUILocalPickerConfig.current.graffitiPaintStrokeWidth.toPx() } val mosaicStrokeWidth = with(LocalDensity.current) { QMUILocalPickerConfig.current.mosaicPaintStrokeWidth.toPx() } val currentLayerState = remember(editLayers, editPaint, layoutInfo, drawable) { val layer = when (editPaint) { is ColorEditPaint -> { GraffitiEditLayer(Path(), editPaint.color, graffitiStrokeWidth / layoutInfo.scale) } is MosaicEditPaint -> { val image = photoInfo.mosaicBitmapCache[editPaint.scaleLevel] ?: drawable.toBitmap( drawable.intrinsicWidth / editPaint.scaleLevel, drawable.intrinsicHeight / editPaint.scaleLevel ).asImageBitmap().also { photoInfo.mosaicBitmapCache[editPaint.scaleLevel] = it } MosaicEditLayer( path = Path(), image = image, strokeWidth = mosaicStrokeWidth ) } } mutableStateOf(layer, neverEqualPolicy()) } val currentLayer = currentLayerState.value Canvas(modifier = Modifier .width(w / layoutInfo.scale) .height(h / layoutInfo.scale) .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.translationX = layoutInfo.rect.left this.translationY = layoutInfo.rect.top this.scaleX = layoutInfo.scale this.scaleY = layoutInfo.scale this.clip = true }) { with(currentLayer) { draw() } } Box( modifier = Modifier .fillMaxSize() .pointerInput(editLayers, editPaint, layoutInfo) { coroutineScope { forEachGesture { awaitPointerEventScope { val down = awaitFirstDown(requireUnconsumed = true) down.consumeDownChange() currentLayer.path.moveTo( (down.position.x - layoutInfo.rect.left) / layoutInfo.scale, (down.position.y - layoutInfo.rect.top) / layoutInfo.scale ) onTouchBegin() do { val event = awaitPointerEvent() val change = event.changes.find { it.id.value == down.id.value } if (change != null) { change.consumePositionChange() currentLayer.path.lineTo( (change.position.x - layoutInfo.rect.left) / layoutInfo.scale, (change.position.y - layoutInfo.rect.top) / layoutInfo.scale ) currentLayerState.value = currentLayer } } while (change == null || change.pressed) onTouchEnd(currentLayer) } } } } ) } @Composable private fun QMUIPhotoPickerEditPaintOptions( editPaint: List, size: Dp, selectedIndex: Int, onSelect: (Int) -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceAround ) { editPaint.forEachIndexed { index, paintEdit -> paintEdit.Compose(size = size, selected = index == selectedIndex) { onSelect(index) } } } } @Composable private fun QMUIPhotoPickerEditToolBar( modifier: Modifier, isPaintState: Boolean, onPaintClick: (toPaint: Boolean) -> Unit, onTextClick: () -> Unit, onClipClick: () -> Unit, onEnsureClick: () -> Unit ) { ConstraintLayout( modifier = modifier .fillMaxWidth() .height(50.dp) ) { val (paint, text, clip, ensure) = createRefs() val horChain = createHorizontalChain(paint, text, clip, chainStyle = ChainStyle.Packed(0f)) constrain(horChain) { start.linkTo(parent.start, 16.dp) end.linkTo(ensure.start) } CommonImageButton( modifier = Modifier .padding(10.dp) .constrainAs(paint) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, res = R.drawable.ic_qmui_checkbox_checked, checked = isPaintState ) { onPaintClick(!isPaintState) } CommonImageButton( modifier = Modifier .padding(10.dp) .constrainAs(text) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, res = R.drawable.ic_qmui_checkbox_checked, ) { onTextClick() } CommonImageButton( modifier = Modifier .padding(10.dp) .constrainAs(clip) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) }, res = R.drawable.ic_qmui_checkbox_checked, ) { onClipClick() } CommonButton( enabled = true, text = "确定", onClick = onEnsureClick, modifier = Modifier.constrainAs(ensure) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) end.linkTo(parent.end, 16.dp) } ) } } @Composable private fun QMUIPhotoPickerEditTextPaintOptions( editPaint: List, size: Dp, color: Color, isReverse: Boolean, modifier: Modifier, onReverseClick: (isReverse: Boolean) -> Unit, onSelect: (Color) -> Unit ) { Row( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { CommonImageButton( res = R.drawable.ic_qmui_mark, modifier = Modifier.padding(16.dp), ) { onReverseClick(!isReverse) } Box( modifier = Modifier .width(OnePx()) .height(size + 8.dp) .background(QMUILocalPickerConfig.current.commonSeparatorColor) ) Row( modifier = Modifier .weight(1f), horizontalArrangement = Arrangement.SpaceAround ) { editPaint.forEach { paintEdit -> paintEdit.Compose(size = size, selected = paintEdit.color == color) { onSelect(paintEdit.color) } } } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Grid.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.border 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.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState 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.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsCompat import com.qmuiteam.compose.core.ex.drawTopSeparator import com.qmuiteam.compose.core.helper.OnePx import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets import com.qmuiteam.compose.core.provider.dp import com.qmuiteam.photo.data.QMUIMediaModel import com.qmuiteam.photo.data.QMUIMediaPhotoVO import kotlinx.coroutines.flow.StateFlow import java.lang.StringBuilder class QMUIPhotoPickerGridRowData(val key: String, val list: List) private fun convertToRowData(data: List, rowCount: Int): List{ val ret = mutableListOf() var list = mutableListOf() val keySb = StringBuilder() data.forEach { keySb.append(it.model.uri) list.add(it) if(list.size == rowCount){ ret.add(QMUIPhotoPickerGridRowData(keySb.toString(), list)) list = mutableListOf() keySb.clear() } } if(list.isNotEmpty()){ ret.add(QMUIPhotoPickerGridRowData(keySb.toString(), list)) } return ret } @Composable fun QMUIPhotoPickerGrid( data: List, modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), pickedItems: List, onPickItem: (toPick: Boolean, model: QMUIMediaPhotoVO) -> Unit, onPreview: (model: QMUIMediaModel) -> Unit ) { BoxWithConstraints(modifier = modifier) { val config = QMUILocalPickerConfig.current val gap = config.gridGap val rowCount = remember(maxWidth, config) { val preferredSize = config.gridPreferredSize ((maxWidth + gap) / (preferredSize + gap)).toInt().coerceAtLeast(2) } val cellSize = remember(maxWidth, gap, rowCount) { ((maxWidth + gap) / rowCount) - gap } val rowData = remember(data, rowCount) { convertToRowData(data, rowCount) } // TODO use LazyVerticalGrid for a replacement LazyColumn( state = state, verticalArrangement = Arrangement.Absolute.spacedBy(gap) ) { items(rowData, key = { it.key }){ item -> QMUIPhotoPickerGridRow(item, cellSize, gap, pickedItems, onPickItem, onPreview) } } } } @Composable private fun QMUIPhotoPickerGridRow( data: QMUIPhotoPickerGridRowData, cellSize: Dp, gap: Dp, pickedItems: List, onPickItem: (toPick: Boolean, model: QMUIMediaPhotoVO) -> Unit, onPreview: (model: QMUIMediaModel) -> Unit ) { Row( modifier = Modifier .fillMaxWidth(), horizontalArrangement = Arrangement.Absolute.spacedBy(gap), ) { for(i in 0 until data.list.size){ QMUIPhotoPickerGridCell( data = data.list[i], cellSize = cellSize, pickedItems = pickedItems, onPickItem = onPickItem, onPreview = onPreview ) } } } @Composable private fun QMUIPhotoPickerGridCell( data: QMUIMediaPhotoVO, cellSize: Dp, pickedItems: List, onPickItem: (toPick: Boolean, model: QMUIMediaPhotoVO) -> Unit, onPreview: (model: QMUIMediaModel) -> Unit ) { val pickedIndex = remember(pickedItems) { pickedItems.indexOfFirst { it == data.model.id } } Box( modifier = Modifier .size(cellSize) .border(OnePx(), QMUILocalPickerConfig.current.gridBorderColor) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = true ) { onPreview.invoke(data.model) } ) { val thumbnail = remember(data) { data.photoProvider.thumbnail(true) } thumbnail?.Compose( contentScale = ContentScale.Crop, isContainerDimenExactly = true, onSuccess = null, onError = null ) QMUIPhotoPickerGridCellMask(pickedIndex) Box( modifier = Modifier .align(Alignment.TopEnd) .clickable { onPickItem(pickedIndex < 0, data) } .padding(4.dp) .size(24.dp), contentAlignment = Alignment.Center ) { QMUIPhotoPickCheckBox(pickedIndex) } } } @Composable fun QMUIPhotoPickerGridCellMask(pickedIndex: Int){ val maskAlpha = animateFloatAsState(targetValue = if(pickedIndex >= 0) 0.36f else 0.15f) Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = maskAlpha.value)) ) } @Composable fun QMUIPhotoPickerGridToolBar( modifier: Modifier = Modifier, enableOrigin: Boolean, pickedItems: List, isOriginOpenFlow: StateFlow, onToggleOrigin: (toOpen: Boolean) -> Unit, onPreview: () -> Unit ) { val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.navigationBars() ).dp() val config = QMUILocalPickerConfig.current Box(modifier = modifier .background(config.toolBarBgColor) .padding(bottom = insets.bottom) .height(44.dp) .drawBehind { drawTopSeparator(config.commonSeparatorColor) } ) { CommonTextButton( modifier = Modifier.align(Alignment.CenterStart), enable = pickedItems.isNotEmpty(), text = "预览", onClick = onPreview ) if(enableOrigin){ OriginOpenButton( modifier = Modifier .fillMaxHeight() .padding(horizontal = 16.dp) .align(Alignment.Center), isOriginOpenFlow = isOriginOpenFlow, onToggleOrigin = onToggleOrigin ) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/PaintEdit.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp sealed class EditPaint { @Composable abstract fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit) } class MosaicEditPaint( val scaleLevel: Int ) : EditPaint() { @Composable override fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit) { val ringWidth = with(LocalDensity.current) { 2.dp.toPx() } androidx.compose.foundation.Canvas(modifier = Modifier .size(size) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onClick() }) { drawCircle( Color.White, radius = this.size.minDimension / 2 - if (selected) 0f else ringWidth ) drawCircle( Color.Black, radius = this.size.minDimension / 2 - ringWidth * 2 ) } } } class ColorEditPaint(val color: Color) : EditPaint() { @Composable override fun Compose(size: Dp, selected: Boolean, onClick: () -> Unit) { val ringWidth = with(LocalDensity.current) { 2.dp.toPx() } androidx.compose.foundation.Canvas(modifier = Modifier .size(size) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onClick() }) { drawCircle( Color.White, radius = this.size.minDimension / 2 - if (selected) 0f else ringWidth ) drawCircle( color, radius = this.size.minDimension / 2 - ringWidth * 2 ) } } } sealed class PaintEditLayer(val path: Path) { abstract fun DrawScope.draw() abstract fun drawToBitmap() } class GraffitiEditLayer( path: Path, val color: Color, val strokeWidth: Float ) : PaintEditLayer(path) { override fun DrawScope.draw() { drawPath( path, color = color, style = Stroke( width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round ) ) } override fun drawToBitmap() { } } class MosaicEditLayer( path: Path, val image: ImageBitmap, val strokeWidth: Float ) : PaintEditLayer(path) { private val paint = Paint() override fun DrawScope.draw() { if (!path.isEmpty) { drawContext.canvas.withSaveLayer(Rect(Offset.Zero, drawContext.size), paint) { drawPath( path, Color.White, style = Stroke( width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round ) ) drawImage( image, dstSize = IntSize( drawContext.size.width.toInt(), drawContext.size.height.toInt() ), blendMode = BlendMode.SrcIn, filterQuality = FilterQuality.None ) } } } override fun drawToBitmap() { } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/Preview.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsCompat import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.PagerState import com.qmuiteam.compose.core.ex.drawTopSeparator import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets import com.qmuiteam.compose.core.provider.dp import com.qmuiteam.photo.compose.QMUIGesturePhoto import com.qmuiteam.photo.data.PhotoLoadStatus import com.qmuiteam.photo.data.QMUIMediaPhotoVO import kotlinx.coroutines.flow.StateFlow @OptIn(ExperimentalPagerApi::class) @Composable fun QMUIPhotoPickerPreview( pagerState: PagerState, data: List, loading: @Composable BoxScope.() -> Unit, loadingFailed: @Composable BoxScope.() -> Unit, onTap: () -> Unit ) { HorizontalPager( count = data.size, state = pagerState ) { page -> val item = data[page] BoxWithConstraints(modifier = Modifier.fillMaxSize()) { QMUIGesturePhoto( containerWidth = maxWidth, containerHeight = maxHeight, imageRatio = item.model.ratio(), shouldTransitionEnter = false, shouldTransitionExit = false, isLongImage = item.photoProvider.isLongImage(), onBeginPullExit = { false }, onTapExit = { onTap() } ) { _, _, _, onImageRatioEnsured -> QMUIPhotoPickerPreviewItemContent(item, onImageRatioEnsured, loadingFailed, loading) } } } } @Composable private fun QMUIPhotoPickerPreviewItemContent( item: QMUIMediaPhotoVO, onImageRatioEnsured: (Float) -> Unit, loading: @Composable BoxScope.() -> Unit, loadingFailed: @Composable BoxScope.() -> Unit, ) { val photo = remember(item) { item.photoProvider.photo() } var loadStatus by remember { mutableStateOf(PhotoLoadStatus.loading) } Box(modifier = Modifier.fillMaxSize()) { photo?.Compose( contentScale = ContentScale.Fit, isContainerDimenExactly = true, onSuccess = { if (it.drawable.intrinsicWidth > 0 && it.drawable.intrinsicHeight > 0) { onImageRatioEnsured(it.drawable.intrinsicWidth.toFloat() / it.drawable.intrinsicHeight) } loadStatus = PhotoLoadStatus.success }, onError = { loadStatus = PhotoLoadStatus.failed } ) if (loadStatus == PhotoLoadStatus.loading) { loading() } else if (loadStatus == PhotoLoadStatus.failed) { loadingFailed() } } } @Composable fun QMUIPhotoPickerPreviewPickedItems( data: List, pickedItems: List, currentId: Long, onClick: (QMUIMediaPhotoVO) -> Unit ) { if (pickedItems.isNotEmpty()) { val list = remember(data, pickedItems) { data.filter { pickedItems.contains(it.model.id) } } LazyRow( modifier = Modifier .fillMaxWidth() .height(60.dp) .background(QMUILocalPickerConfig.current.toolBarBgColor), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(5.dp), contentPadding = PaddingValues(horizontal = 5.dp) ) { items(list, { it.model.id }) { QMUIPhotoPickerPreviewPickedItem(it, it.model.id == currentId, onClick) } } } } @Composable private fun QMUIPhotoPickerPreviewPickedItem( item: QMUIMediaPhotoVO, isCurrent: Boolean, onClick: (QMUIMediaPhotoVO) -> Unit ) { val thumb = remember(item) { item.photoProvider.thumbnail(true) } Box(modifier = Modifier .size(50.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { onClick(item) } .let { if (isCurrent) { it.border(2.dp, QMUILocalPickerConfig.current.commonIconCheckedTintColor) } else { it } } ) { thumb?.Compose( contentScale = ContentScale.Crop, isContainerDimenExactly = true, onSuccess = null, onError = null ) } } @Composable fun QMUIPhotoPickerPreviewToolBar( modifier: Modifier = Modifier, current: QMUIMediaPhotoVO, isCurrentPicked: Boolean, enableOrigin: Boolean, isOriginOpenFlow: StateFlow, onToggleOrigin: (toOpen: Boolean) -> Unit, onEdit: () -> Unit, onToggleSelect: (toSelect: Boolean) -> Unit ) { val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.navigationBars() ).dp() val config = QMUILocalPickerConfig.current Box(modifier = modifier .background(config.toolBarBgColor) .padding(bottom = insets.bottom) .height(44.dp) .drawBehind { drawTopSeparator(config.commonSeparatorColor) } ) { if (current.model.editable && config.editable) { CommonTextButton( modifier = Modifier.align(Alignment.CenterStart), enable = true, text = "编辑", onClick = onEdit ) } if (enableOrigin) { OriginOpenButton( modifier = Modifier .fillMaxHeight() .padding(horizontal = 16.dp) .align(Alignment.Center), isOriginOpenFlow = isOriginOpenFlow, onToggleOrigin = onToggleOrigin ) } PickCurrentCheckButton( modifier = Modifier .fillMaxHeight() .padding(horizontal = 16.dp) .align(Alignment.CenterEnd), isPicked = isCurrentPicked, onPicked = onToggleSelect ) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/TextEdit.kt ================================================ package com.qmuiteam.photo.compose.picker import android.graphics.Typeface import android.text.TextPaint import android.util.Log import androidx.compose.animation.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.input.pointer.consumeAllChanges import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChangeConsumed import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.view.WindowInsetsCompat import com.qmuiteam.compose.core.R import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets import com.qmuiteam.compose.core.provider.dp import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlin.math.PI import kotlin.math.abs import kotlin.math.absoluteValue internal class TextEditLayer( val text: String, val fontSize: TextUnit, val center: Offset, val color: Color, val reverse: Boolean, val offsetFlow: MutableStateFlow = MutableStateFlow(Offset.Zero), val scaleFlow: MutableStateFlow = MutableStateFlow(1f), val rotationFlow: MutableStateFlow = MutableStateFlow(0f) ) { val isFocusFlow = MutableStateFlow(false) private val textColor = if (reverse) { (if (color == Color.White) Color.Black else Color.White) } else color private val paint = TextPaint().apply { typeface = Typeface.DEFAULT_BOLD color = textColor.toArgb() setShadowLayer(0f, 2f, 2f, textColor.copy(0.4f).toArgb()) } @Composable private fun TextLayout( modifier: Modifier, lineSpace: Float, paddingHor: Float, paddingVer: Float, fontSize: Float, isFocus: Boolean ) { val cornerRadius = with(LocalDensity.current) { 10.dp.toPx() } val focusPointSize = with(LocalDensity.current) { 6.dp.toPx() } val focusLineWidth = with(LocalDensity.current) { 2.dp.toPx() } Canvas(modifier = modifier) { val rectTopLeftOffset = Offset(focusPointSize / 2, focusPointSize / 2) val rectSize = Size(size.width - focusPointSize, size.height - focusPointSize) if (reverse) { drawRoundRect( color, topLeft = rectTopLeftOffset, size = rectSize, cornerRadius = CornerRadius(cornerRadius, cornerRadius) ) } paint.textSize = fontSize drawIntoCanvas { val fontHeight = paint.descent() - paint.ascent() var baseLine = paddingVer - paint.ascent() var start = 0 while (start < text.length) { val count = paint.breakText( text, start, text.length, false, size.width - paddingHor * 2, null ) val end = start + count val contentWidth = paint.measureText(text, start, end) it.nativeCanvas.drawText(text, start, end, (size.width - contentWidth) / 2, baseLine, paint) baseLine += fontHeight + lineSpace start = end } } if (isFocus) { drawRect( Color.White, topLeft = rectTopLeftOffset, size = rectSize, style = Stroke(focusLineWidth) ) val focusSize = Size(focusPointSize, focusPointSize) drawRect( Color.White, topLeft = Offset.Zero, size = focusSize ) drawRect( Color.White, topLeft = Offset(size.width - focusPointSize, 0f), size = focusSize ) drawRect( Color.White, topLeft = Offset(0f, size.height - focusPointSize), size = focusSize ) drawRect( Color.White, topLeft = Offset(size.width - focusPointSize, size.height - focusPointSize), size = focusSize ) } } } @OptIn(ExperimentalComposeUiApi::class) @Composable fun Content( layoutInfo: PickerPhotoLayoutInfo, onFocus: () -> Unit, onEdit: () -> Unit, onToggleDragging: (Boolean) -> Unit, onDelete: () -> Unit ) { val currentOffset by offsetFlow.collectAsState() val currentRotation by rotationFlow.collectAsState() val currentScale by scaleFlow.collectAsState() val lineSpace = with(LocalDensity.current) { QMUILocalPickerConfig.current.textEditLineSpace.toPx() } val fontSizePx = with(LocalDensity.current) { fontSize.toPx() } val paddingHor = with(LocalDensity.current) { 16.dp.toPx() } val paddingVer = with(LocalDensity.current) { 8.dp.toPx() } val isFocus by isFocusFlow.collectAsState() BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val (contentWidth, contentHeight) = remember(constraints.maxWidth, constraints.maxHeight, fontSizePx) { paint.textSize = fontSizePx val textConstraintMaxWidth = constraints.maxWidth - paddingHor * 4 val fontHeight = paint.descent() - paint.ascent() var start = 0 var textMaxWidth = 0f var lineCount = 0 while (start < text.length) { val count = paint.breakText( text, start, text.length, false, textConstraintMaxWidth, null ) val end = start + count val contentWidth = paint.measureText(text, start, end) textMaxWidth = textMaxWidth.coerceAtLeast(contentWidth) lineCount++ start = end } arrayOf( textMaxWidth + paddingHor * 2, lineCount * (fontHeight + lineSpace) - lineSpace + paddingVer * 2 ) } val contentWidthDp = with(LocalDensity.current) { contentWidth.toDp() } val contentHeightDp = with(LocalDensity.current) { contentHeight.toDp() } val start = with(LocalDensity.current) { (center.x - contentWidth / 2).toDp() } val top = with(LocalDensity.current) { (center.y - contentHeight / 2).toDp() } val dragInfo = remember { MutableDragInfo() } var isDragging by remember { mutableStateOf(false) } var isInDeleteArea by remember { mutableStateOf(false) } TextLayout( modifier = Modifier .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) scaleX = layoutInfo.scale scaleY = layoutInfo.scale translationX = layoutInfo.rect.left translationY = layoutInfo.rect.top } .padding(start = start, top = top) .width(contentWidthDp) .height(contentHeightDp) .onGloballyPositioned { dragInfo.editLayerCenter = it.positionInWindow() + Offset(it.size.width / 2f, it.size.height / 2f) } .graphicsLayer { translationX = currentOffset.x translationY = currentOffset.y scaleX = currentScale scaleY = currentScale rotationZ = currentRotation } .pointerInput("") { coroutineScope { launch { detectTapGestures( onTap = { if (isFocusFlow.value) { onEdit() } else { isFocusFlow.value = true onFocus() } }, ) } launch { forEachGesture { awaitPointerEventScope { var rotation = 0f var zoom = 1f var pan = Offset.Zero var pastTouchSlop = false val touchSlop = viewConfiguration.touchSlop awaitFirstDown(requireUnconsumed = false) do { val event = awaitPointerEvent() val canceled = event.changes.any { it.positionChangeConsumed() } if (!canceled) { val zoomChange = event.calculateZoom() val rotationChange = event.calculateRotation() val panChange = event.calculatePan() if (!pastTouchSlop) { zoom *= zoomChange rotation += rotationChange pan += panChange val centroidSize = event.calculateCentroidSize(useCurrent = false) val zoomMotion = abs(1 - zoom) * centroidSize val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f) val panMotion = pan.getDistance() if (zoomMotion > touchSlop || rotationMotion > touchSlop || panMotion > touchSlop ) { pastTouchSlop = true } } if (pastTouchSlop) { if (rotationChange != 0f || zoomChange != 1f || panChange != Offset.Zero ) { if(panChange != Offset.Zero){ if(!isDragging){ isDragging = true onToggleDragging(true) } } offsetFlow.value = offsetFlow.value + panChange scaleFlow.value = scaleFlow.value * zoomChange rotationFlow.value = rotationFlow.value + rotationChange if (isDragging) { isInDeleteArea = dragInfo.isInDeleteArea(offsetFlow.value) } } event.changes.forEach { if (it.positionChanged()) { it.consumeAllChanges() } } } } } while (!canceled && event.changes.any { it.pressed }) if (isDragging) { if (isInDeleteArea) { onDelete() } } isInDeleteArea = false isDragging = false onToggleDragging(false) } } } } }, lineSpace = lineSpace, paddingHor = paddingHor, paddingVer = paddingVer, fontSize = fontSizePx, isFocus = isFocus ) AnimatedVisibility( visible = isDragging, modifier = Modifier.align(Alignment.BottomCenter), enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() ) { DeleteArea(isInDeleteArea) { offset, size -> dragInfo.deleteAreaOffset = offset dragInfo.deleteAreaSize = size } } } } @Composable private fun DeleteArea( isFocusing: Boolean, onPlaced: (offset: Offset, size: IntSize) -> Unit ) { val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.navigationBars() ).dp() val config = QMUILocalPickerConfig.current Column(modifier = Modifier .padding(bottom = insets.bottom + 16.dp) .clip(RoundedCornerShape(8.dp)) .onGloballyPositioned { onPlaced(it.positionInWindow(), it.size) } .background(if (isFocusing) config.editLayerDeleteAreaNormalFocusColor else config.editLayerDeleteAreaNormalBgColor) .padding(horizontal = 24.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Image( painter = painterResource( id = if (isFocusing) { R.drawable.ic_qmui_checkbox_checked } else R.drawable.ic_qmui_checkbox_partial ), contentDescription = "", colorFilter = ColorFilter.tint(Color.White) ) Spacer(modifier = Modifier.height(10.dp)) Text( text = if (isFocusing) "松手即可删除" else "拖动到此处删除", color = Color.White, fontSize = 15.sp ) } } } private class MutableDragInfo( var deleteAreaOffset: Offset = Offset.Zero, var deleteAreaSize: IntSize = IntSize.Zero, var editLayerCenter: Offset = Offset.Zero ) { fun isInDeleteArea(offset: Offset): Boolean { val windowOffset = editLayerCenter + offset return windowOffset.x > deleteAreaOffset.x && windowOffset.x < deleteAreaOffset.x + deleteAreaSize.width && windowOffset.y > deleteAreaOffset.y && windowOffset.y < deleteAreaOffset.y + deleteAreaSize.height } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/compose/picker/TopBarItem.kt ================================================ package com.qmuiteam.photo.compose.picker import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.Canvas 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.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.qmuiteam.compose.core.ui.QMUITopBarItem import kotlinx.coroutines.flow.StateFlow class QMUIPhotoPickerBucketTopBarItem( private val bgColor: Color, private val textColor: Color, private val iconBgColor: Color, private val iconColor: Color, private val textFlow: StateFlow, private val isFocusFlow: StateFlow, private val onClick: () -> Unit ) : QMUITopBarItem { @Composable override fun Compose(topBarHeight: Dp) { val text by textFlow.collectAsState() Row( modifier = Modifier .clip(CircleShape) .background(bgColor) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = true, ) { onClick() } .padding(start = 12.dp, end = 6.dp, top = 4.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Absolute.spacedBy(5.dp) ) { Text( text, fontSize = 17.sp, color = textColor, modifier = Modifier.padding(bottom = 1.dp) ) QMUIPhotoPickerBucketToggleArrow(iconBgColor, iconColor, isFocusFlow) } } } class QMUIPhotoSendTopBarItem( private val canSendSelf: Boolean, private val text: String, private val maxSelectCount: Int, private val selectCountFlow: StateFlow, private val onClick: () -> Unit ) : QMUITopBarItem { @Composable override fun Compose(topBarHeight: Dp) { val selectCount by selectCountFlow.collectAsState() CommonButton( enabled = selectCount > 0 || canSendSelf, text = if (selectCount > 0) "$text($selectCount/$maxSelectCount)" else text, onClick = onClick ) } } @Composable fun QMUIPhotoPickerBucketToggleArrow( bgColor: Color, iconColor: Color, isFocusFlow: StateFlow ) { val isFocus by isFocusFlow.collectAsState() Box( modifier = Modifier .size(20.dp) .clip(CircleShape) .background(bgColor), contentAlignment = Alignment.Center ) { val strokeWidth = with(LocalDensity.current) { 1.6.dp.toPx() } val transition = updateTransition(targetState = isFocus, "QMUIPhotoPickerBucketToggleArrow") val rotate = transition.animateFloat( transitionSpec = { tween(durationMillis = 300) }, label = "QMUIPhotoPickerBucketToggleArrowFocus" ) { if (it) 180f else 0f } Canvas( modifier = Modifier .width(8.dp) .height(4.dp) .rotate(rotate.value) ) { drawPath(Path().apply { moveTo(0f, 0f) lineTo(size.width / 2, size.height) lineTo(size.width, 0f) }, iconColor, style = Stroke(strokeWidth)) } } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt ================================================ package com.qmuiteam.photo.data import android.graphics.* import android.graphics.drawable.Drawable import android.os.Build import android.util.LruCache import androidx.compose.ui.unit.IntSize import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.InputStream import kotlin.math.max import kotlin.math.min fun interface QMUIBitmapRegionLoader { suspend fun load(): Bitmap? } class QMUIBitmapRegionProvider( val width: Int, val height: Int, val loader: QMUIBitmapRegionLoader ) class QMUIAlreadyBitmapRegionLoader(private val bm: Bitmap) : QMUIBitmapRegionLoader { override suspend fun load(): Bitmap { return bm } } private class QMUICacheBitmapRegionLoader( private val origin: QMUIBitmapRegionLoader, private val cacheStatistic: QMUIBitmapRegionCacheStatistic ) : QMUIBitmapRegionLoader { @Volatile private var cache: Bitmap? = null private val mutex = Mutex() override suspend fun load(): Bitmap? { val localCache = cache if (localCache != null) { return localCache } return mutex.withLock { if (cache != null) { return cache } origin.load().also { cache = it cacheStatistic.doWhenLoaded(this) } } } suspend fun releaseCache() { mutex.withLock { cache = null } } } class QMUIBitmapRegion(val width: Int, val height: Int, val list: List) /** * fit: * if ture, fit the image to the dst so that both dimensions (width and height) of the image will be equal to or less than the dst * if false, fill the image in the dst such that both dimensions (width and height) of the image will be equal to or larger than the dst */ fun loadLongImageThumbnail( ins: InputStream, preferredSize: IntSize, options: BitmapFactory.Options, fit: Boolean = false, ): Bitmap? { return loadLongImage(ins, preferredSize, options, fit) { regionDecoder -> val w = regionDecoder.width val h = regionDecoder.height val pageHeight = if (preferredSize.width > 0 && preferredSize.height > 0) { (w * preferredSize.height / preferredSize.width).coerceAtMost(w * 5).coerceAtMost(h) } else { (5 * w).coerceAtMost(h) } regionDecoder.decodeRegion(Rect(0, 0, w, pageHeight), options) } } /** * fit: * if ture, fit the image to the dst so that both dimensions (width and height) of the image will be equal to or less than the dst * if false, fill the image in the dst such that both dimensions (width and height) of the image will be equal to or larger than the dst */ fun loadLongImage( ins: InputStream, preferredSize: IntSize, options: BitmapFactory.Options, fit: Boolean = false, preloadCount: Int = Int.MAX_VALUE, cacheTimeoutForLazyLoad: Long = 1000, cacheCountForLazyLoad: Int = 5 ): QMUIBitmapRegion { val cacheStatistic = QMUIBitmapRegionCacheStatistic(cacheTimeoutForLazyLoad, cacheCountForLazyLoad) return loadLongImage(ins, preferredSize, options, fit) { regionDecoder -> val w = regionDecoder.width val h = regionDecoder.height val pageHeight = if (preferredSize.width > 0 && preferredSize.height > 0) { (w * preferredSize.height / preferredSize.width).coerceAtMost(w * 5).coerceAtMost(h) } else { (5 * w).coerceAtMost(h) } val ret = arrayListOf() var top = 0 var i = 0 while (top < h) { val bottom = (top + pageHeight).coerceAtMost(h) if (i < preloadCount) { val bm = regionDecoder.decodeRegion(Rect(0, top, w, bottom), options) ret.add(QMUIBitmapRegionProvider(bm.width, bm.height, QMUIAlreadyBitmapRegionLoader(bm))) } else { val finalTop = top val loader = object : QMUIBitmapRegionLoader { private val mutex = Mutex() override suspend fun load(): Bitmap? { return mutex.withLock { regionDecoder.decodeRegion(Rect(0, finalTop, w, bottom), options) } } } ret.add( QMUIBitmapRegionProvider( w, bottom - finalTop, if (cacheStatistic.canCache()) { QMUICacheBitmapRegionLoader(loader, cacheStatistic) } else { loader } ) ) } top = bottom i++ } QMUIBitmapRegion(w, h, ret) } } private fun loadLongImage( ins: InputStream, preferredSize: IntSize, options: BitmapFactory.Options, fit: Boolean = false, handler: (BitmapRegionDecoder) -> T ): T { // Read the image's dimensions. options.inJustDecodeBounds = true val bufferedIns = ins.buffered() bufferedIns.mark(Int.MAX_VALUE) BitmapFactory.decodeStream(bufferedIns, null, options) options.inJustDecodeBounds = false bufferedIns.reset() options.inMutable = false if (options.outWidth > 0 && options.outHeight > 0) { val dstWidth = if (preferredSize.width <= 0) options.outWidth else preferredSize.width val dstHeight = if (preferredSize.height <= 0) options.outHeight else preferredSize.height options.inSampleSize = calculateInSampleSize( srcWidth = options.outWidth, srcHeight = options.outHeight, dstWidth = dstWidth, dstHeight = dstHeight, fit = fit ) } else { options.inSampleSize = 1 } val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BitmapRegionDecoder.newInstance(bufferedIns) } else { BitmapRegionDecoder.newInstance(bufferedIns, false) } checkNotNull(regionDecoder) { "BitmapRegionDecoder newInstance failed." } return handler(regionDecoder) } private fun calculateInSampleSize( srcWidth: Int, srcHeight: Int, dstWidth: Int, dstHeight: Int, fit: Boolean = false ): Int { val widthInSampleSize = Integer.highestOneBit(srcWidth / dstWidth) val heightInSampleSize = Integer.highestOneBit(srcHeight / dstHeight) return if (fit) { max(widthInSampleSize, heightInSampleSize).coerceAtLeast(1) } else { min(widthInSampleSize, heightInSampleSize).coerceAtLeast(1) } } private class QMUIBitmapRegionCacheStatistic( val cacheTimeoutForLazyLoad: Long, val cacheCountForLazyLoad: Int ) { private val scope = CoroutineScope(Dispatchers.IO) private val cacheJobs = object : LruCache(cacheCountForLazyLoad) { override fun entryRemoved(evicted: Boolean, key: QMUICacheBitmapRegionLoader?, oldValue: Job?, newValue: Job?) { super.entryRemoved(evicted, key, oldValue, newValue) if (newValue == null) { key?.let { scope.launch { it.releaseCache() } } } else { oldValue?.cancel() } } } fun doWhenLoaded(loader: QMUICacheBitmapRegionLoader) { val job = scope.launch { delay(cacheTimeoutForLazyLoad) cacheJobs.remove(loader) } cacheJobs.put(loader, job) } fun canCache(): Boolean { return cacheTimeoutForLazyLoad > 0 && cacheCountForLazyLoad > 0 } } class QMUIBitmapRegionHolderDrawable(val bitmapRegion: QMUIBitmapRegion) : Drawable() { override fun getIntrinsicHeight(): Int { return bitmapRegion.height } override fun getIntrinsicWidth(): Int { return bitmapRegion.width } override fun draw(canvas: Canvas) { } override fun setAlpha(alpha: Int) { } override fun setColorFilter(colorFilter: ColorFilter?) { } override fun getOpacity(): Int { return PixelFormat.OPAQUE } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/data/QMUIMediaDataProvider.kt ================================================ package com.qmuiteam.photo.data import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.MediaStore import androidx.core.database.getIntOrNull import androidx.core.database.getLongOrNull import androidx.core.database.getStringOrNull import com.qmuiteam.compose.core.helper.QMUILog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File const val QMUIMediaPhotoBucketAllId = "__all__" const val QMUIMediaPhotoBucketAllName = "最近项目" open class QMUIMediaModel( val id: Long, val uri: Uri, var width: Int, var height: Int, val rotation: Int, val name: String, val modifyTimeSec: Long, val bucketId: String, val bucketName: String, val editable: Boolean ) { fun ratio(): Float { if(height <= 0 || width <= 0){ return -1f } if(rotation == 90 || rotation == 270){ return height.toFloat() / width } return width.toFloat() / height } } class QMUIMediaPhotoBucket( val id: String, val name: String, val list: List ) class QMUIMediaPhotoBucketVO( val id: String, val name: String, val list: List ) class QMUIMediaPhotoVO( val model: QMUIMediaModel, val photoProvider: QMUIPhotoProvider ) interface QMUIMediaPhotoProviderFactory { fun factory(model: QMUIMediaModel): QMUIPhotoProvider } interface QMUIMediaDataProvider { suspend fun provide(context: Context, supportedMimeTypes: Array): List } class QMUIMediaImagesProvider : QMUIMediaDataProvider { companion object { private const val TAG = "QMUIMediaDataProvider" val DEFAULT_SUPPORT_MIMETYPES = arrayOf( "image/jpeg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif" ) private val COLUMNS = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.HEIGHT, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_MODIFIED, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_DISPLAY_NAME ) } override suspend fun provide(context: Context, supportedMimeTypes: Array): List { return withContext(Dispatchers.IO) { val selection = if (supportedMimeTypes.isEmpty()) { null } else { val sb = StringBuilder() sb.append(MediaStore.Images.Media.MIME_TYPE) sb.append(" IN (") supportedMimeTypes.forEachIndexed { index, s -> if (index != 0) { sb.append(",") } sb.append("'") sb.append(s) sb.append("'") } sb.append(")") sb.toString() } val list = mutableListOf() context.applicationContext.contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, COLUMNS, selection, null, "${MediaStore.Images.Media.DATE_MODIFIED} DESC" )?.use { cursor -> if (cursor.moveToFirst()) { do { try { val path = cursor.readString(MediaStore.Images.Media.DATA) val id = cursor.readLong(MediaStore.Images.Media._ID) val w = cursor.readInt(MediaStore.Images.Media.WIDTH) val h = cursor.readInt(MediaStore.Images.Media.HEIGHT) val o = cursor.readInt(MediaStore.Images.Media.ORIENTATION) val isRotated = o == 90 || o == 270 list.add( QMUIMediaModel( id, ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), if(isRotated) h else w, if(isRotated) w else h, cursor.readInt(MediaStore.Images.Media.ORIENTATION), cursor.readString(MediaStore.Images.Media.DISPLAY_NAME), cursor.readLong(MediaStore.Images.Media.DATE_MODIFIED), cursor.readString(MediaStore.Images.Media.BUCKET_ID), (cursor.readString(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)).let { it.ifEmpty { File(path).parent ?: "" } }, true ) ) } catch (e: Exception) { QMUILog.e(TAG, "read image data from cursor failed.", e) } } while (cursor.moveToNext()) } } val buckets = mutableListOf() val defaultPhotoBucket = MutableMediaPhotoBucket(QMUIMediaPhotoBucketAllId, QMUIMediaPhotoBucketAllName) buckets.add(defaultPhotoBucket) list.forEach { model -> defaultPhotoBucket.list.add(model) if(model.name.isNotBlank()){ val bucket = buckets.find { it.id == model.bucketId} ?:MutableMediaPhotoBucket(model.bucketId, model.bucketName).also { buckets.add(it) } bucket.list.add(model) } } buckets.map { QMUIMediaPhotoBucket(it.id, it.name, it.list) } } } private class MutableMediaPhotoBucket( val id: String, val name: String ){ val list: MutableList = mutableListOf() } } private fun Cursor.getColumnIndexAndDoAction(columnName: String, block: (Int) -> T): T? { return try { getColumnIndexOrThrow(columnName).let { if (it < 0) null else block(it) } } catch (e: Throwable) { QMUILog.e("QMUIMediaDataProvider", "getColumnIndex for $columnName failed.", e) null } } fun Cursor.readLong(columnName: String): Long = getColumnIndexAndDoAction(columnName) { getLongOrNull(it) } ?: 0 fun Cursor.readString(columnName: String): String = getColumnIndexAndDoAction(columnName) { getStringOrNull(it) } ?: "" fun Cursor.readInt(columnName: String): Int = getColumnIndexAndDoAction(columnName) { getIntOrNull(it) } ?: 0 ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionDelivery.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.photo.data import androidx.annotation.MainThread internal object QMUIPhotoTransitionDelivery { private val dataMap = mutableMapOf() @MainThread fun put(data: PhotoViewerData): Long { val id = System.currentTimeMillis() dataMap[id] = data // memory leak protection val iterator = dataMap.iterator() while (iterator.hasNext()) { val next = iterator.next() if (next.key < id - 20 * 1000) { iterator.remove() } } return id } @MainThread fun peek(id: Long): PhotoViewerData? { return dataMap[id] } @MainThread fun getAndRemove(id: Long): PhotoViewerData? { return dataMap.remove(id) } @MainThread fun remove(id: Long) { dataMap.remove(id) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransitionInfo.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.photo.data import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize import com.qmuiteam.photo.compose.QMUILocalPhotoConfig class PhotoViewerData( val list: List, val index: Int, val background: Bitmap? ) internal enum class PhotoLoadStatus { loading, success, failed } class PhotoResult(val model: Any, val drawable: Drawable) interface QMUIPhoto { @Composable fun Compose( contentScale: ContentScale, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) } interface QMUIPhotoProvider { fun thumbnail(openBlankColor: Boolean): QMUIPhoto? fun photo(): QMUIPhoto? fun ratio(): Float = -1f fun isLongImage(): Boolean = false fun meta(): Bundle? fun recoverCls(): Class? } class QMUIPhotoTransitionInfo( val photoProvider: QMUIPhotoProvider, var offsetInWindow: Offset?, var size: IntSize?, var photo: Drawable? ) { fun photoRect(): Rect? { val offset = offsetInWindow val size = size?.toSize() if (offset == null || size == null || size.width == 0f || size.height == 0f) { return null } return Rect(offset, size) } fun ratio(): Float { var ratio = photoProvider.ratio() if (ratio <= 0f) { photo?.let { if (it.intrinsicWidth > 0 && it.intrinsicHeight > 0) { ratio = it.intrinsicWidth.toFloat() / it.intrinsicHeight } } } return ratio } } val lossPhotoProvider = object : QMUIPhotoProvider { override fun thumbnail(openBlankColor: Boolean): QMUIPhoto? { return null } override fun photo(): QMUIPhoto? { return null } override fun meta(): Bundle? { return null } override fun recoverCls(): Class? { return null } } val lossPhotoTransitionInfo = QMUIPhotoTransitionInfo(lossPhotoProvider, null, null, null) interface PhotoTransitionProviderRecover { fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? } class ImageItem( val url: String, val thumbnailUrl: String?, val thumbnail: Bitmap? ) ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/util/BitmapEx.kt ================================================ package com.qmuiteam.photo.util import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.util.Log import androidx.core.net.toUri import com.qmuiteam.compose.core.helper.QMUILog import java.io.* val DefaultBitmapCompressMaxSizeStrategy: (Bitmap) -> Int = { val ratio = it.width.toFloat() / it.height if (ratio < 0.33 || ratio > 3) { 1024 * 1024 * 8 } else { 1024 * 1024 * 2 } } val DefaultBitmapCompressCanUseMemoryStorage: (Bitmap) -> Boolean = { it.width * it.height < 1080 * 1920 } abstract class BitmapCompressResult internal constructor( val compressFormat: Bitmap.CompressFormat, val compressQuality: Int, val width: Int, val height: Int, ) { abstract fun inputStream(): InputStream? } internal class BitmapCompressStreamResult( compressFormat: Bitmap.CompressFormat, compressQuality: Int, width: Int, height: Int, private val stream: BitmapCompressStream ): BitmapCompressResult(compressFormat, compressQuality, width, height){ override fun inputStream(): InputStream? { return stream.inputStream() } } fun Bitmap.saveToLocal( dir: File, compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, compressQuality: Int = 80, ): Uri{ val suffix = when(compressFormat){ Bitmap.CompressFormat.JPEG -> "jpeg" Bitmap.CompressFormat.PNG -> "png" else -> "webp" } val fileName = "qmui_photo_${System.nanoTime()}.${suffix}" dir.mkdirs() val destFile = File(dir, fileName) destFile.outputStream().buffered().use { compress(compressFormat, compressQuality, it) } return destFile.toUri() } fun Bitmap.compressByShortEdgeWidthAndByteSize( context: Context, shortEdgeMaxWidth: Int = 1200, byteMaxSizeStrategy: (Bitmap) -> Int = DefaultBitmapCompressMaxSizeStrategy, canUseMemoryStorage: (Bitmap) -> Boolean = DefaultBitmapCompressCanUseMemoryStorage, compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, compressQuality: Int = 80, ): BitmapCompressResult? { var bitmap = this try { val ratio = width.toFloat() / height if (width <= height) { if (width > shortEdgeMaxWidth) { bitmap = Bitmap.createScaledBitmap(this, shortEdgeMaxWidth, (shortEdgeMaxWidth / ratio).toInt(), false) } } else { if (height > shortEdgeMaxWidth) { bitmap = Bitmap.createScaledBitmap(this, (shortEdgeMaxWidth * ratio).toInt(), shortEdgeMaxWidth, false) } } } catch (ignored: OutOfMemoryError) { QMUILog.w( "compressByShortEdgeWidthAndByteSize", "createScaledBitmap failed: shortEdgeMaxWidth = $shortEdgeMaxWidth, width = $width; height = $height" ) } val byteMaxSize = byteMaxSizeStrategy(this) val useMemoryStorage = canUseMemoryStorage(this) val stream: BitmapCompressStream = if (useMemoryStorage) BitmapCompressMemoryStream() else BitmapCompressFileStream(context.cacheDir) var currentQuality = compressQuality var nextQuality = currentQuality var failCount = 0 var succes: Boolean do { stream.reset() currentQuality = nextQuality succes = try { stream.outputStream().use { bitmap.compress(compressFormat, currentQuality, it) } } catch (e: Throwable) { QMUILog.w( "compressByShortEdgeWidthAndByteSize", "compress bitmap failed(compressFormat = $compressFormat; quality = $nextQuality, failCount = $failCount).", e ) false } if (succes) { nextQuality -= 10 failCount = 0 } else { nextQuality -= 5 failCount++ } } while ((!succes && failCount < 2 && nextQuality >= 20) || (succes && nextQuality >= 20 && stream.size() > byteMaxSize)) if (!succes) { return null } return BitmapCompressStreamResult(compressFormat, currentQuality, bitmap.width, bitmap.height, stream) } internal interface BitmapCompressStream { fun reset() fun size(): Int fun outputStream(): OutputStream fun inputStream(): InputStream? } internal class BitmapCompressMemoryStream : BitmapCompressStream { private val output = ByteArrayOutputStream() override fun reset() { output.reset() } override fun size(): Int { return output.size() } override fun outputStream(): OutputStream { return output } override fun inputStream(): InputStream { return ByteArrayInputStream(output.toByteArray()) } } internal class BitmapCompressFileStream(val cacheDir: File) : BitmapCompressStream { private var file: File? = null override fun reset() { file?.delete() file = File(cacheDir, "qmui-bm-${System.nanoTime()}") } override fun size(): Int { return file?.length()?.toInt() ?: 0 } override fun outputStream(): OutputStream { return file!!.outputStream().buffered() } override fun inputStream(): InputStream? { return file?.inputStream()?.buffered() } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/util/QMUIPhotoHelper.kt ================================================ package com.qmuiteam.photo.util import android.content.ContentValues import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore import android.util.Log import java.io.IOException import java.io.InputStream import java.io.OutputStream object QMUIPhotoHelper { private const val TAG = "QMUIPhotoHelper" fun saveToStore( context: Context, bitmap: Bitmap, nameWithoutSuffix: String, dirName: String = Environment.DIRECTORY_PICTURES, compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, compressQuality: Int = 100 ): Uri? { val suffix = when (compressFormat) { Bitmap.CompressFormat.JPEG -> ".jpeg" Bitmap.CompressFormat.PNG -> ".png" else -> ".webp" } val mime = when (compressFormat) { Bitmap.CompressFormat.JPEG -> "image/jpeg" Bitmap.CompressFormat.PNG -> "image/png" else -> "image/webp" } return saveToStore(context, "$nameWithoutSuffix$suffix", mime, dirName) { bitmap.compress(compressFormat, compressQuality, it) } } fun saveToStore( context: Context, name: String, mimeType: String, dirName: String = Environment.DIRECTORY_PICTURES, writer: (OutputStream) -> Unit ): Uri? { val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis()) put(MediaStore.MediaColumns.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, dirName) put(MediaStore.MediaColumns.IS_PENDING, 1) } } val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI var stream: OutputStream? = null var uri: Uri? = null try { uri = context.contentResolver.insert(contentUri, contentValues) if (uri == null) { throw IOException("Failed to create new MediaStore record.") } stream = context.contentResolver.openOutputStream(uri) if (stream == null) { throw IOException("Failed to get output stream.") } writer.invoke(stream) contentValues.clear() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) context.contentResolver.update(uri, contentValues, null, null) } return uri } catch (e: Throwable) { Log.i(TAG, "saveToStore failed.", e) if (uri != null) { context.contentResolver.delete(uri, null, null) } } finally { stream?.close() } return null } fun compressByShortEdgeWidthAndByteSize( context: Context, originProvider: (Context) -> InputStream?, shortEdgeMaxWidth: Int = 1200, byteMaxSizeStrategy: (Bitmap) -> Int = DefaultBitmapCompressMaxSizeStrategy, canUseMemoryStorage: (Bitmap) -> Boolean = DefaultBitmapCompressCanUseMemoryStorage, compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, compressQuality: Int = 80 ): BitmapCompressResult? { val applicationContext = context.applicationContext val options = BitmapFactory.Options() options.inJustDecodeBounds = true var inputStream = originProvider(applicationContext) ?: return null inputStream.use { BitmapFactory.decodeStream(it, null, options) } val imageHeight = options.outHeight val imageWidth = options.outWidth if (imageWidth <= imageHeight) { if (imageWidth > shortEdgeMaxWidth) { options.inSampleSize = Integer.highestOneBit(imageWidth / shortEdgeMaxWidth) } } else { if (imageHeight > shortEdgeMaxWidth) { options.inSampleSize = Integer.highestOneBit(imageHeight / shortEdgeMaxWidth) } } options.inJustDecodeBounds = false inputStream = originProvider(applicationContext) ?: return null val bitmap = inputStream.use { BitmapFactory.decodeStream(it, null, options) } ?: return object : BitmapCompressResult(compressFormat, -1, -1, -1) { override fun inputStream(): InputStream? { return originProvider(applicationContext) } } return bitmap.compressByShortEdgeWidthAndByteSize( context, shortEdgeMaxWidth, byteMaxSizeStrategy, canUseMemoryStorage, compressFormat, compressQuality ) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt ================================================ package com.qmuiteam.photo.util import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.os.Build import android.util.Size import android.view.View import android.view.WindowManager import android.widget.ImageView fun View.asBitmap(): Bitmap? { if (this is ImageView) { val drawable = drawable if (drawable != null && drawable is BitmapDrawable) { return drawable.bitmap } } return try { clearFocus() val bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas() canvas.setBitmap(bm) canvas.save() draw(canvas) canvas.restore() canvas.setBitmap(null) bm } catch (e: Throwable) { e.printStackTrace() null } } fun getWindowSize(context: Context): Size { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val windowMetrics = wm.currentWindowMetrics Size(windowMetrics.bounds.width(), windowMetrics.bounds.height()) } else { val displayMetrics = context.resources.displayMetrics Size(displayMetrics.widthPixels, displayMetrics.heightPixels) } } ================================================ FILE: photo/src/main/java/com/qmuiteam/photo/vm/QMUIPhotoPickerViewModel.kt ================================================ package com.qmuiteam.photo.vm import android.app.Application import android.net.Uri import androidx.annotation.Keep import androidx.compose.foundation.lazy.LazyListState import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.qmuiteam.compose.core.helper.LogTag import com.qmuiteam.compose.core.helper.QMUILog import com.qmuiteam.photo.activity.* import com.qmuiteam.photo.data.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.ArrayList class QMUIPhotoPickerViewModel @Keep constructor( val application: Application, val state: SavedStateHandle, val dataProvider: QMUIMediaDataProvider, val supportedMimeTypes: Array ) : ViewModel(), LogTag { val pickLimitCount = state.get(QMUI_PHOTO_PICK_LIMIT_COUNT) ?: QMUI_PHOTO_DEFAULT_PICK_LIMIT_COUNT val enableOrigin = state.get(QMUI_PHOTO_ENABLE_ORIGIN) ?: true private val photoProviderFactory: QMUIMediaPhotoProviderFactory private val _photoPickerSceneFlow = MutableStateFlow(QMUIPhotoPickerGridScene) val photoPickerSceneFlow = _photoPickerSceneFlow.asStateFlow() val gridSceneScrollState = LazyListState() var prevScene: QMUIPhotoPickerScene? = null private set private val _photoPickerDataFlow = MutableStateFlow(QMUIPhotoPickerData(QMUIPhotoPickerLoadState.permissionChecking, null)) val photoPickerDataFlow = _photoPickerDataFlow.asStateFlow() private val _pickedMap = mutableMapOf() private val _pickedListFlow = MutableStateFlow>(emptyList()) val pickedListFlow = _pickedListFlow.asStateFlow() private val _pickedCountFlow = MutableStateFlow(0) val pickedCountFlow = _pickedCountFlow.asStateFlow() private val _isOriginOpenFlow = MutableStateFlow(false) val isOriginOpenFlow = _isOriginOpenFlow.asStateFlow() init { val photoProviderFactoryClsName = state.get(QMUI_PHOTO_PROVIDER_FACTORY) ?: throw RuntimeException("no QMUIMediaPhotoProviderFactory is provided.") photoProviderFactory = Class.forName(photoProviderFactoryClsName).newInstance() as QMUIMediaPhotoProviderFactory } fun updateScene(scene: QMUIPhotoPickerScene) { prevScene = _photoPickerSceneFlow.value _photoPickerSceneFlow.value = scene } fun permissionDenied() { _photoPickerDataFlow.value = QMUIPhotoPickerData(QMUIPhotoPickerLoadState.permissionDenied, null) } fun permissionGranted() { _photoPickerDataFlow.value = QMUIPhotoPickerData(QMUIPhotoPickerLoadState.dataLoading, null) viewModelScope.launch { try { val data = withContext(Dispatchers.IO) { dataProvider.provide(application, supportedMimeTypes).map { bucket -> QMUIMediaPhotoBucketVO(bucket.id, bucket.name, bucket.list.map { QMUIMediaPhotoVO(it, photoProviderFactory.factory(it)) }) } } val pickedItems = state.get>(QMUI_PHOTO_PICKED_ITEMS) if(pickedItems != null){ state.set(QMUI_PHOTO_PICKED_ITEMS, null) val map = mutableMapOf() _pickedMap.clear() data.find { it.id == QMUIMediaPhotoBucketAllId}?.list?.let { list -> for(element in list){ if(pickedItems.find { it == element.model.uri } != null) { _pickedMap[element.model.id] = element map[element.model.uri] = element.model.id } if(map.size == pickedItems.size){ break } } } // keep the order. val list = pickedItems.mapNotNull { map[it] } _pickedListFlow.value = list _pickedCountFlow.value = list.size } _photoPickerDataFlow.value = QMUIPhotoPickerData(QMUIPhotoPickerLoadState.dataLoaded, data) } catch (e: Throwable) { _photoPickerDataFlow.value = QMUIPhotoPickerData(QMUIPhotoPickerLoadState.dataLoaded, null, e) } } } fun toggleOrigin(toOpen: Boolean) { _isOriginOpenFlow.value = toOpen } fun togglePick(item: QMUIMediaPhotoVO) { if (_photoPickerDataFlow.value.state != QMUIPhotoPickerLoadState.dataLoaded) { QMUILog.w(TAG, "pick when data is not finish loaded, please check why this method called here?") return } val list = arrayListOf() list.addAll(_pickedListFlow.value) if (list.contains(item.model.id)) { _pickedMap.remove(item.model.id) list.remove(item.model.id) _pickedListFlow.value = list _pickedCountFlow.value = list.size } else { if (list.size >= pickLimitCount) { QMUILog.w(TAG, "can not pick more photo, please check why this method called here?") return } _pickedMap[item.model.id] = item list.add(item.model.id) _pickedListFlow.value = list _pickedCountFlow.value = list.size } } fun getPickedVOList(): List{ return _pickedListFlow.value.mapNotNull { id -> _pickedMap[id] } } fun getPickedResultList(): List { return _pickedListFlow.value.mapNotNull { id -> _pickedMap[id]?.model?.let { QMUIPhotoPickItemInfo(it.id, it.name, it.width, it.height, it.uri, it.rotation) } } } } open class QMUIPhotoPickerScene object QMUIPhotoPickerGridScene : QMUIPhotoPickerScene() class QMUIPhotoPickerPreviewScene( val buckedId: String, val onlySelected: Boolean, val currentId: Long ) : QMUIPhotoPickerScene() class QMUIPhotoPickerEditScene( val current: QMUIMediaPhotoVO ) : QMUIPhotoPickerScene() enum class QMUIPhotoPickerLoadState { permissionChecking, permissionDenied, dataLoading, dataLoaded } class QMUIPhotoPickerData( val state: QMUIPhotoPickerLoadState, val data: List?, val error: Throwable? = null ) ================================================ FILE: photo/src/main/res/anim/scale_enter.xml ================================================ ================================================ FILE: photo/src/main/res/anim/scale_exit.xml ================================================ ================================================ FILE: photo/src/test/java/com/qmuiteam/ExampleUnitTest.kt ================================================ package com.qmuiteam /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { } ================================================ FILE: photo-coil/.gitignore ================================================ /build ================================================ FILE: photo-coil/build.gradle.kts ================================================ import com.qmuiteam.plugin.Dep plugins { id("com.android.library") kotlin("android") `maven-publish` signing id("qmui-publish") } version = Dep.QMUI.photoVer 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 freeCompilerArgs += "-Xjvm-default=all" } } dependencies { implementation(project(":compose-core")) implementation(Dep.AndroidX.coreKtx) api(project(":photo")) api(Dep.Coil.compose) } ================================================ FILE: photo-coil/consumer-rules.pro ================================================ ================================================ FILE: photo-coil/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: photo-coil/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt ================================================ package com.qmuiteam 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.test", appContext.packageName) } } ================================================ FILE: photo-coil/src/main/AndroidManifest.xml ================================================ ================================================ FILE: photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt ================================================ package com.qmuiteam.photo.coil import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import androidx.compose.ui.unit.IntSize import coil.ImageLoader import coil.decode.BitmapFactoryDecoder import coil.decode.DecodeResult import coil.decode.Decoder import coil.decode.ImageSource import coil.fetch.SourceResult import coil.request.Options import coil.request.get import coil.size.Scale import coil.size.pxOrElse import com.qmuiteam.photo.data.QMUIBitmapRegionHolderDrawable import com.qmuiteam.photo.data.loadLongImage import com.qmuiteam.photo.data.loadLongImageThumbnail import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit class QMUICoilImageDecoderFactory(maxParallelism: Int = 4) : Decoder.Factory { companion object { val defaultInstance by lazy { QMUICoilImageDecoderFactory() } } private val parallelismLock = Semaphore(maxParallelism) override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? { return if ((options.parameters["isLongImage"] as? Boolean) == true) { QMUICoilLongImageDecoder(result.source, options, parallelismLock) } else { BitmapFactoryDecoder(result.source, options, parallelismLock) } } } class QMUICoilLongImageDecoder( private val source: ImageSource, private val options: Options, private val parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE) ) : Decoder { private val isThumb = options.parameters["isThumb"] == true override suspend fun decode(): DecodeResult = parallelismLock.withPermit { runInterruptible { decode(BitmapFactory.Options()) } } private fun decode(bmOptions: BitmapFactory.Options): DecodeResult { val ins = source.source().inputStream() val (width, height) = options.size val dstWidth = width.pxOrElse { -1 } val dstHeight = height.pxOrElse { -1 } if (isThumb) { val bm = loadLongImageThumbnail(ins, IntSize(dstWidth, dstHeight), bmOptions, options.scale == Scale.FIT) return DecodeResult( drawable = BitmapDrawable(options.context.resources, bm), isSampled = bmOptions.inSampleSize > 1 ) } else { val bitmapRegion = loadLongImage( ins, IntSize(dstWidth, dstHeight), bmOptions, options.scale == Scale.FIT, preloadCount = 2 ) return DecodeResult( drawable = QMUIBitmapRegionHolderDrawable(bitmapRegion), isSampled = bmOptions.inSampleSize > 1 || bmOptions.inScaled ) } } } ================================================ FILE: photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt ================================================ package com.qmuiteam.photo.coil import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.core.graphics.drawable.toBitmap import coil.compose.AsyncImage import coil.compose.AsyncImageContent import coil.compose.AsyncImagePainter import coil.imageLoader import coil.request.ErrorResult import coil.request.ImageRequest import coil.request.SuccessResult import coil.size.Scale import com.qmuiteam.photo.compose.BlankBox import com.qmuiteam.photo.compose.QMUIBitmapRegionItem import com.qmuiteam.photo.data.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext open class QMUICoilThumbPhoto( val uri: Uri, val isLongImage: Boolean, val openBlankColor: Boolean ) : QMUIPhoto { @Composable override fun Compose( contentScale: ContentScale, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) { if (isLongImage) { LongImage(onSuccess, onError, openBlankColor) } else { val context = LocalContext.current val model = remember(context, uri, onSuccess, onError) { ImageRequest.Builder(context) .data(uri) .allowHardware(false) .crossfade(true) .decoderFactory(QMUICoilImageDecoderFactory.defaultInstance) .listener(onError = { _, result -> onError?.invoke(result.throwable) }) { _, result -> onSuccess?.invoke(PhotoResult(uri, result.drawable)) }.build() } AsyncImage( model = model, contentDescription = "", contentScale = if (isContainerDimenExactly) contentScale else ContentScale.Inside, alignment = Alignment.Center, modifier = Modifier.let { if (isContainerDimenExactly) { it.fillMaxSize() } else { it } } ) { state -> if (state == AsyncImagePainter.State.Empty || state is AsyncImagePainter.State.Loading) { if (isContainerDimenExactly && openBlankColor) { BlankBox() } } else { AsyncImageContent() } } } } @Composable fun LongImage( onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)?, openBlankColor: Boolean ) { BoxWithConstraints(Modifier.fillMaxSize()) { val request = ImageRequest.Builder(LocalContext.current) .allowHardware(false) .setParameter("isThumb", true) .setParameter("isLongImage", true) .crossfade(true) .decoderFactory(QMUICoilImageDecoderFactory.defaultInstance) .data(uri) .scale(Scale.FILL) .size(constraints.maxWidth, constraints.maxHeight) .build() LongImageContent(request, onSuccess, onError, openBlankColor) } } @Composable fun LongImageContent( request: ImageRequest, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)?, openBlankColor: Boolean ) { val imageLoader = LocalContext.current.imageLoader var bitmap by remember("") { mutableStateOf(null) } LaunchedEffect("") { withContext(Dispatchers.IO) { val result = imageLoader.execute(request) if (result is SuccessResult) { bitmap = result.drawable.toBitmap() withContext(Dispatchers.Main) { onSuccess?.invoke(PhotoResult(uri, result.drawable)) } } else if (result is ErrorResult) { withContext(Dispatchers.Main) { onError?.invoke(result.throwable) } } } } val bm = bitmap if (bm != null) { Image( painter = BitmapPainter(bm.asImageBitmap()), contentDescription = "", contentScale = ContentScale.FillWidth, alignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize() ) } else if (openBlankColor) { BlankBox() } } } class QMUICoilPhoto( val uri: Uri, val isLongImage: Boolean ) : QMUIPhoto { @Composable override fun Compose( contentScale: ContentScale, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) { if (isLongImage) { LongImage(onSuccess, onError) } else { val context = LocalContext.current val model = remember(context, uri, onSuccess, onError) { ImageRequest.Builder(context) .data(uri) .allowHardware(false) .crossfade(true) .decoderFactory(QMUICoilImageDecoderFactory.defaultInstance) .listener(onError = { _, result -> onError?.invoke(result.throwable) }) { _, result -> onSuccess?.invoke(PhotoResult(uri, result.drawable)) }.build() } AsyncImage( model = model, contentDescription = "", contentScale = contentScale, alignment = Alignment.Center, modifier = Modifier.let { if (isContainerDimenExactly) { it.fillMaxSize() } else { it } } ) } } @Composable fun LongImage( onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) { var images by remember { mutableStateOf(emptyList()) } val context = LocalContext.current LaunchedEffect(key1 = "") { val result = withContext(Dispatchers.IO) { val request = ImageRequest.Builder(context) .data(uri) .crossfade(true) .setParameter("isLongImage", true) .decoderFactory(QMUICoilImageDecoderFactory.defaultInstance) .build() context.imageLoader.execute(request) } if (result is SuccessResult) { (result.drawable as? QMUIBitmapRegionHolderDrawable)?.bitmapRegion?.let { images = it.list } onSuccess?.invoke(PhotoResult(uri, result.drawable)) } else if (result is ErrorResult) { onError?.invoke(result.throwable) } } if (images.isNotEmpty()) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(images) { image -> BoxWithConstraints() { val width = constraints.maxWidth val height = width * image.height / image.width val heightDp = with(LocalDensity.current) { height.toDp() } QMUIBitmapRegionItem(image, maxWidth, heightDp) } } } } } } open class QMUICoilPhotoProvider( val uri: Uri, val thumbUri: Uri, val ratio: Float ) : QMUIPhotoProvider { companion object { const val META_URI_KEY = "meta_uri" const val META_THUMB_URI_KEY = "meta_thumb_uri" const val META_RATIO_KEY = "meta_ratio" } constructor(uri: Uri, ratio: Float) : this(uri, uri, ratio) override fun thumbnail(openBlankColor: Boolean): QMUIPhoto? { return QMUICoilThumbPhoto(thumbUri, isLongImage(), openBlankColor) } override fun photo(): QMUIPhoto? { return QMUICoilPhoto(uri, isLongImage()) } override fun ratio(): Float { return ratio } override fun isLongImage(): Boolean { return ratio > 0 && ratio < 0.2f } override fun meta(): Bundle? { return Bundle().apply { putParcelable(META_URI_KEY, uri) if(thumbUri != uri){ putParcelable(META_THUMB_URI_KEY, thumbUri) } putParcelable(META_THUMB_URI_KEY, thumbUri) putFloat(META_RATIO_KEY, ratio) } } override fun recoverCls(): Class? { return QMUICoilPhotoTransitionProviderRecover::class.java } } class QMUICoilPhotoTransitionProviderRecover : PhotoTransitionProviderRecover { override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? { val uri = bundle.getParcelable(QMUICoilPhotoProvider.META_URI_KEY) ?: return null val thumbUri = bundle.getParcelable(QMUICoilPhotoProvider.META_THUMB_URI_KEY) ?: uri val ratio = bundle.getFloat(QMUICoilPhotoProvider.META_RATIO_KEY) return QMUIPhotoTransitionInfo( QMUICoilPhotoProvider(uri, thumbUri, ratio), null, null, null ) } } ================================================ FILE: photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUIMediaCoilPhotoProviderFactory.kt ================================================ package com.qmuiteam.photo.coil import com.qmuiteam.photo.data.QMUIMediaModel import com.qmuiteam.photo.data.QMUIMediaPhotoProviderFactory import com.qmuiteam.photo.data.QMUIPhotoProvider class QMUIMediaCoilPhotoProviderFactory : QMUIMediaPhotoProviderFactory { override fun factory(model: QMUIMediaModel): QMUIPhotoProvider { return QMUICoilPhotoProvider( model.uri, model.ratio() ) } } ================================================ FILE: photo-coil/src/test/java/com/qmuiteam/ExampleUnitTest.kt ================================================ package com.qmuiteam /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { } ================================================ FILE: photo-glide/.gitignore ================================================ /build ================================================ FILE: photo-glide/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.photoVer 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 freeCompilerArgs += "-Xjvm-default=all" } } dependencies { implementation(project(":compose-core")) implementation(Dep.AndroidX.coreKtx) api(project(":photo")) api(Dep.Glide.glide) kapt(Dep.Glide.compiler) } ================================================ FILE: photo-glide/consumer-rules.pro ================================================ ================================================ FILE: photo-glide/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: photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt ================================================ package com.qmuiteam 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.test", appContext.packageName) } } ================================================ FILE: photo-glide/src/main/AndroidManifest.xml ================================================ ================================================ FILE: photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt ================================================ package com.qmuiteam.photo.glide import android.content.Context import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.compose.ui.unit.IntSize import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.load.Option import com.bumptech.glide.load.Options import com.bumptech.glide.load.ResourceDecoder import com.bumptech.glide.load.engine.Resource import com.bumptech.glide.load.resource.SimpleResource import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.module.LibraryGlideModule import com.qmuiteam.photo.data.QMUIBitmapRegionHolderDrawable import com.qmuiteam.photo.data.loadLongImage import com.qmuiteam.photo.data.loadLongImageThumbnail import java.io.IOException import java.io.InputStream import java.nio.ByteBuffer val QMUI_PHOTO_IMG_IS_THUMB = Option.memory("com.qmuiteam.photo.isThumb", false) class QMUILongGlidePhotoData( val drawable: Drawable ) @GlideModule class QMUIGlideModule : LibraryGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { registry.prepend( Registry.BUCKET_BITMAP, InputStream::class.java, QMUILongGlidePhotoData::class.java, object : ResourceDecoder { override fun handles(source: InputStream, options: Options): Boolean { return true } override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource { return doDecode(context, source, width, height, options) } }) } } private fun doDecode( context: Context, source: InputStream, width: Int, height: Int, options: Options ): Resource { val isThumb = options.get(QMUI_PHOTO_IMG_IS_THUMB) == true val bmOptions = BitmapFactory.Options() if (isThumb) { val bm = loadLongImageThumbnail( source, IntSize(width, height), bmOptions, options.get(DownsampleStrategy.OPTION) == DownsampleStrategy.CENTER_INSIDE ) val drawable = BitmapDrawable(context.resources, bm) return SimpleResource(QMUILongGlidePhotoData(drawable)) } else { val bitmapRegion = loadLongImage( source, IntSize(width, height), bmOptions, options.get(DownsampleStrategy.OPTION) == DownsampleStrategy.CENTER_INSIDE, preloadCount = 2 ) val drawable = QMUIBitmapRegionHolderDrawable(bitmapRegion) return SimpleResource(QMUILongGlidePhotoData(drawable)) } } private class ByteBufferInputStream(val buf: ByteBuffer) : InputStream() { @Throws(IOException::class) override fun read(): Int { return if (!buf.hasRemaining()) { -1 } else buf.get().toInt() and 0xFF } @Throws(IOException::class) override fun read(bytes: ByteArray, off: Int, len: Int): Int { if (!buf.hasRemaining()) { return -1 } val toRead = len.coerceAtMost(buf.remaining()) buf.get(bytes, off, toRead) return toRead } } ================================================ FILE: photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt ================================================ package com.qmuiteam.photo.glide import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.os.SystemClock import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.core.graphics.drawable.toBitmap import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.qmuiteam.photo.compose.BlankBox import com.qmuiteam.photo.compose.QMUIBitmapRegionItem import com.qmuiteam.photo.compose.QMUILocalPhotoConfig import com.qmuiteam.photo.data.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable private fun GlideImage( uri: Uri, isLongImage: Boolean, isThumbImage: Boolean, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: (() -> Unit)?, contentDescription: String = "", contentScale: ContentScale = ContentScale.Fit, openBlankColor: Boolean = false ) { BoxWithConstraints(modifier = if (isContainerDimenExactly) Modifier.fillMaxSize() else Modifier) { val state = remember(uri) { mutableStateOf?>(null) } val context = LocalContext.current Log.i("cginetest", "1. $constraints") DisposableEffect(uri, isContainerDimenExactly, constraints.isZero,isLongImage, isThumbImage, contentScale) { val key = SystemClock.elapsedRealtime() val request = when { constraints.isZero -> null isLongImage -> { Glide.with(context).`as`(QMUILongGlidePhotoData::class.java).load(uri) .downsample(DownsampleStrategy.CENTER_OUTSIDE) .dontTransform() .set(QMUI_PHOTO_IMG_IS_THUMB, isThumbImage) .into(object : CustomTarget( constraints.maxWidth, constraints.maxHeight ) { override fun onResourceReady(resource: QMUILongGlidePhotoData, transition: Transition?) { state.value = key to resource.drawable onSuccess?.invoke(PhotoResult(uri, resource.drawable)) } override fun onLoadStarted(placeholder: Drawable?) { if (placeholder != null || state.value?.first == key) { state.value = -1L to placeholder } } override fun onLoadCleared(placeholder: Drawable?) { if (state.value?.first == key) { state.value = -1L to placeholder } } override fun onLoadFailed(errorDrawable: Drawable?) { onError?.invoke() } }) .request } else -> { Glide.with(context).load(uri) .downsample(DownsampleStrategy.AT_LEAST) .dontTransform() .into(object : CustomTarget( constraints.maxWidth, constraints.maxHeight ) { override fun onResourceReady(resource: Drawable, transition: Transition?) { state.value = key to resource onSuccess?.invoke(PhotoResult(uri, resource)) } override fun onLoadStarted(placeholder: Drawable?) { if (placeholder != null || state.value?.first == key) { state.value = -1L to placeholder } } override fun onLoadCleared(placeholder: Drawable?) { if (state.value?.first == key) { state.value = -1L to placeholder } } override fun onLoadFailed(errorDrawable: Drawable?) { onError?.invoke() } }) .request } } onDispose { request?.clear() } } val currentDrawable = state.value?.second if (currentDrawable != null) { if (currentDrawable is QMUIBitmapRegionHolderDrawable) { LongImageContent(currentDrawable) } else { Image( modifier = if (isContainerDimenExactly) { Modifier.fillMaxSize() } else Modifier, contentDescription = contentDescription, painter = BitmapPainter(currentDrawable.toBitmap().asImageBitmap()), contentScale = contentScale, ) } } else if (isContainerDimenExactly && openBlankColor) { BlankBox() } } } @Composable private fun LongImageContent(drawable: QMUIBitmapRegionHolderDrawable) { val images by remember(drawable) { mutableStateOf(drawable.bitmapRegion.list) } if (images.isNotEmpty()) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(images) { image -> BoxWithConstraints() { val width = constraints.maxWidth val height = width * image.height / image.width val heightDp = with(LocalDensity.current) { height.toDp() } QMUIBitmapRegionItem(image, maxWidth, heightDp) } } } } } open class QMUIGlideThumbPhoto( val uri: Uri, val isLongImage: Boolean, val openBlankColor: Boolean = true, ) : QMUIPhoto { @Composable override fun Compose( contentScale: ContentScale, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) { GlideImage( uri, isLongImage, true, isContainerDimenExactly, onSuccess, onError = { onError?.invoke(RuntimeException("glide failed to load thumb image.")) }, contentScale = contentScale, openBlankColor = openBlankColor ) } } class QMUIGlidePhoto( val uri: Uri, val isLongImage: Boolean ) : QMUIPhoto { @Composable override fun Compose( contentScale: ContentScale, isContainerDimenExactly: Boolean, onSuccess: ((PhotoResult) -> Unit)?, onError: ((Throwable) -> Unit)? ) { GlideImage( uri, isLongImage, false, isContainerDimenExactly, onSuccess, onError = { onError?.invoke(RuntimeException("glide failed to load thumb image.")) }, contentScale = contentScale ) } } open class QMUIGlidePhotoProvider(val uri: Uri, val thumbUrl: Uri, val ratio: Float) : QMUIPhotoProvider { companion object { const val META_URI_KEY = "meta_uri" const val META_THUMB_URI_KEY = "meta_thumb_uri" const val META_RATIO_KEY = "meta_ratio" } constructor(uri: Uri, ratio: Float): this(uri, uri, ratio) override fun thumbnail(openBlankColor: Boolean): QMUIPhoto? { return QMUIGlideThumbPhoto(thumbUrl, isLongImage(), openBlankColor) } override fun photo(): QMUIPhoto? { return QMUIGlidePhoto(uri, isLongImage()) } override fun ratio(): Float { return ratio } override fun isLongImage(): Boolean { return ratio > 0 && ratio < 0.2f } override fun meta(): Bundle? { return Bundle().apply { putParcelable(META_URI_KEY, uri) if(thumbUrl != uri){ putParcelable(META_THUMB_URI_KEY, thumbUrl) } putFloat(META_RATIO_KEY, ratio) } } override fun recoverCls(): Class? { return QMUIGlidePhotoTransitionProviderRecover::class.java } } class QMUIGlidePhotoTransitionProviderRecover : PhotoTransitionProviderRecover { override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? { val uri = bundle.getParcelable(QMUIGlidePhotoProvider.META_URI_KEY) ?: return null val thumbUri = bundle.getParcelable(QMUIGlidePhotoProvider.META_THUMB_URI_KEY) ?: uri val ratio = bundle.getFloat(QMUIGlidePhotoProvider.META_RATIO_KEY) return QMUIPhotoTransitionInfo( QMUIGlidePhotoProvider(uri, thumbUri, ratio), null, null, null ) } } ================================================ FILE: photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt ================================================ package com.qmuiteam.photo.glide import com.qmuiteam.photo.data.QMUIMediaModel import com.qmuiteam.photo.data.QMUIMediaPhotoProviderFactory import com.qmuiteam.photo.data.QMUIPhotoProvider class QMUIMediaGlidePhotoProviderFactory : QMUIMediaPhotoProviderFactory { override fun factory(model: QMUIMediaModel): QMUIPhotoProvider { return QMUIGlidePhotoProvider( model.uri, model.ratio() ) } } ================================================ FILE: photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt ================================================ package com.qmuiteam /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { } ================================================ FILE: plugin/.gitignore ================================================ /build *.iml .DS_Store .gradle .gradletasknamecache .idea ================================================ FILE: plugin/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-gradle-plugin` idea kotlin("jvm") version "1.6.20" `kotlin-dsl` } buildscript { repositories { mavenCentral() google() mavenLocal() } dependencies { classpath("com.android.tools.build:gradle:7.1.3") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20") } } group = "com.qmuiteam.qmui.plugin" version = "0.0.1" gradlePlugin { plugins { create("qmui-dep"){ id = "qmui-dep" implementationClass = "com.qmuiteam.plugin.QMUIDepPlugin" } create("qmui-publish"){ id = "qmui-publish" implementationClass = "com.qmuiteam.plugin.QMUIPublish" } } } repositories { mavenCentral() google() } dependencies { api(gradleApi()) api(gradleKotlinDsl()) api(kotlin("gradle-plugin", version = "1.6.20")) api(kotlin("gradle-plugin-api", version = "1.6.20")) api("com.android.tools.build:gradle-api:7.1.3") api("com.android.tools.build:gradle:7.1.3") } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } val compileKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { jvmTarget = "11" } ================================================ FILE: plugin/settings.gradle.kts ================================================ ================================================ FILE: plugin/src/main/java/com/qmuiteam/plugin/Dep.kt ================================================ package com.qmuiteam.plugin import org.gradle.api.JavaVersion object Dep { val javaVersion = JavaVersion.VERSION_11 const val kotlinJvmTarget = "11" const val compileSdk = 31 const val minSdk = 21 const val targetSdk = 31 object QMUI { const val group = "com.qmuiteam" const val qmuiVer = "2.1.0.4" const val archVer = "2.1.0.3" const val typeVer = "0.1.0.5" // composeMajor.composeMinor.qmuiReleaseNumber const val composeCoreVer = "1.1.1" const val composeVer = "1.1.1" const val photoVer = "1.1.1.1" const val editorVer = "1.1.1" } object Coroutines { private const val version = "1.6.0" const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version" } object AndroidX { val appcompat = "androidx.appcompat:appcompat:1.4.0" val annotation = "androidx.annotation:annotation:1.3.0" val coreKtx = "androidx.core:core-ktx:1.7.0" val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.2" val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" val activity = "androidx.activity:activity-ktx:1.4.0" val fragment = "androidx.fragment:fragment:1.4.1" } object Compose { val version = "1.2.0-alpha08" val animation = "androidx.compose.animation:animation:$version" val ui = "androidx.compose.ui:ui:$version" val material = "androidx.compose.material:material:$version" val compiler = "androidx.compose.compiler:compiler:$version" val activity = "androidx.activity:activity-compose:1.4.0" val constraintlayout = "androidx.constraintlayout:constraintlayout-compose:1.0.0" val pager = "com.google.accompanist:accompanist-pager:0.23.1" } object Flipper { private const val version = "0.96.1" const val soLoader = "com.facebook.soloader:soloader:0.10.1" const val flipper = "com.facebook.flipper:flipper:$version" } object MaterialDesign { const val material = "com.google.android.material:material:1.4.0" } object CodeGen { const val javapoet = "com.squareup:javapoet:1.13.0" const val autoService = "com.google.auto.service:auto-service:1.0-rc2" } object ButterKnife { private const val ver = "10.1.0" const val butterknife = "com.jakewharton:butterknife:$ver" const val compiler = "com.jakewharton:butterknife-compiler:$ver" } object Coil { const val compose = "io.coil-kt:coil-compose:2.0.0-alpha06" } object Glide { private const val ver = "4.13.0" const val glide = "com.github.bumptech.glide:glide:$ver" const val compiler = "com.github.bumptech.glide:compiler:$ver" } } ================================================ FILE: plugin/src/main/java/com/qmuiteam/plugin/QMUIDepPlugin.kt ================================================ package com.qmuiteam.plugin import org.gradle.api.Plugin import org.gradle.api.Project class QMUIDepPlugin: Plugin{ override fun apply(project: Project) { } } ================================================ FILE: plugin/src/main/java/com/qmuiteam/plugin/QMUIPublish.kt ================================================ package com.qmuiteam.plugin import com.android.build.gradle.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.create import org.gradle.plugins.signing.SigningExtension import java.io.File import java.util.* import kotlin.io.* class QMUIPublish : Plugin { override fun apply(project: Project) { val isAndroid = project.hasProperty("android") if (isAndroid) { println("android") val android = project.extensions.getByName("android") as LibraryExtension android.publishing { singleVariant("release") { withJavadocJar() withSourcesJar() } } } else { println("java/kotlin") project.configure { withSourcesJar() withJavadocJar() } } project.afterEvaluate { val properties = Properties() val file = File(project.rootProject.file("gradle"), "deploy.properties") if (file.exists()) { properties.load(file.inputStream()) val mavenUrl = properties.getProperty("maven.url") val mavenUsername = properties.getProperty("maven.username") val mavenPassword = properties.getProperty("maven.password") println("mavenUrl:$mavenUrl") project.configure { repositories { maven { setUrl(mavenUrl) credentials { username = mavenUsername password = mavenPassword } } } publications { create("release") { project.configure { sign(this@create) } if (isAndroid) { from(components.getByName("release")) } else { from(components.getByName("java")) } groupId = project.group as String artifactId = project.name version = project.version as String pom { name.set("${project.group}:${project.name}") url.set("https://github.com/Tencent/QMUI_Android") description.set("qmui android library.") licenses { license { name.set(properties.getProperty("license.name")) url.set(properties.getProperty("license.url")) } } developers { developer { id.set(properties.getProperty("developer.id")) name.set(properties.getProperty("developer.name")) email.set(properties.getProperty("developer.email")) } } scm { connection.set("scm:git:git://github.com/Tencent/QMUI_Android.git") developerConnection.set("scm:git:ssh://github.com/Tencent/QMUI_Android.git") url.set("https://qmuiteam.com/android") } } } } } } } } } fun println(log: String) { kotlin.io.println("qmui config publish > $log") } ================================================ FILE: qmui/.gitignore ================================================ /*.bin /*.iml /.DS_Store /.gradletasknamecache /.idea /bin /build /local.properties ================================================ FILE: qmui/build.gradle.kts ================================================ import com.qmuiteam.plugin.Dep plugins { id("com.android.library") kotlin("android") `maven-publish` signing id("qmui-publish") } version = Dep.QMUI.qmuiVer 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.annotation) api(Dep.AndroidX.constraintLayout) api(Dep.AndroidX.swiperefreshlayout) api(Dep.MaterialDesign.material) } ================================================ FILE: qmui/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/chant/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # 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 *; #} ================================================ FILE: qmui/src/main/AndroidManifest.xml ================================================ ================================================ FILE: qmui/src/main/assets/QMUIWebviewBridge.js ================================================ (function(){ var doc = document; if(window.QMUIBridge){ return; } var messagingIframe = createIframe(doc); var sendingMessageQueue = []; var receivedMessageQueue = []; var messageHandlers = {}; var QUEUE_HAS_MESSAGE = 'qmui://__QUEUE_MESSAGE__/'; var responseCallbacks = {}; var uuid = 1; function createIframe(doc) { var iframe = doc.createElement('iframe'); iframe.style.display = 'none'; doc.documentElement.appendChild(iframe); return iframe; } function send(data, callback) { if(!data){ throw new Error("message == null") } var message = { data: data } if(callback){ var callbackId = 'cb_' + (uuid++) + '_' + (new Date() - 0); responseCallbacks[callbackId] = callback; message.callbackId = callbackId; } sendingMessageQueue.push(message); messagingIframe.src = QUEUE_HAS_MESSAGE; } function isCmdSupport(cmd, callback){ if(isCmdSupport.__cache && isCmdSupport.__cache.indexOf(cmd) >= 0){ callback(true) return } getSupportedCmdList(function(data){ if(data && data.length > 0){ if(!isCmdSupport.__cache){ isCmdSupport.__cache = [] } for(var i = 0; i < data.length; i++){ isCmdSupport.__cache.push(data[i]) } } callback(isCmdSupport.__cache.indexOf(cmd) >= 0) }) } function getSupportedCmdList(callback){ if(getSupportedCmdList.__cache){ callback(getSupportedCmdList.__cache) return } send({__cmd__: "getSupportedCmdList"}, function(data){ getSupportedCmdList.__cache = data callback(data) }) } function _fetchQueueFromNative(){ var messageQueueString = JSON.stringify(sendingMessageQueue); sendingMessageQueue = []; return messageQueueString; } function _handleResponseFromNative(response){ if(response && response.callbackId){ var responseCallback = responseCallbacks[response.callbackId]; if(responseCallback){ responseCallback(response.data); delete responseCallbacks[response.callbackId]; } } } var QMUIBridge = window.QMUIBridge = { send: send, isCmdSupport: isCmdSupport, getSupportedCmdList: getSupportedCmdList, _fetchQueueFromNative: _fetchQueueFromNative, _handleResponseFromNative: _handleResponseFromNative }; var readyEvent = doc.createEvent('Events'); readyEvent.initEvent('QMUIBridgeReady'); readyEvent.bridge = QMUIBridge; doc.dispatchEvent(readyEvent); })() ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/Beta.java ================================================ package com.qmuiteam.qmui; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Beta { } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/QMUIConfig.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; public class QMUIConfig { public static boolean DEBUG = false; } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/QMUIInterpolatorStaticHolder.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; import androidx.interpolator.view.animation.FastOutLinearInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; /** * @author cginechen * @date 2017-09-02 */ public class QMUIInterpolatorStaticHolder { public static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); public static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); public static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); public static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); public static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); public static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator(); public static final Interpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); public static final Interpolator QUNITIC_INTERPOLATOR = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/QMUILog.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; /** * * @author cginechen * @date 2016-08-11 */ public class QMUILog { public interface QMUILogDelegate { void e(final String tag, final String msg, final Object ... obj); void w(final String tag, final String msg, final Object ... obj); void i(final String tag, final String msg, final Object ... obj); void d(final String tag, final String msg, final Object ... obj); void printErrStackTrace(String tag, Throwable tr, final String format, final Object ... obj); } private static QMUILogDelegate sDelegete = null; public static void setDelegete(QMUILogDelegate delegete) { sDelegete = delegete; } public static void e(final String tag, final String msg, final Object ... obj) { if (sDelegete != null) { sDelegete.e(tag, msg, obj); } } public static void w(final String tag, final String msg, final Object ... obj) { if (sDelegete != null) { sDelegete.w(tag, msg, obj); } } public static void i(final String tag, final String msg, final Object ... obj) { if (sDelegete != null) { sDelegete.i(tag, msg, obj); } } public static void d(final String tag, final String msg, final Object ... obj) { if (sDelegete != null) { sDelegete.d(tag, msg, obj); } } public static void printErrStackTrace(String tag, Throwable tr, final String format, final Object ... obj) { if (sDelegete != null) { sDelegete.printErrStackTrace(tag, tr, format, obj); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaButton.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.alpha; import android.content.Context; import androidx.appcompat.widget.AppCompatButton; import android.util.AttributeSet; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaButton extends AppCompatButton implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaButton(Context context) { super(context); } public QMUIAlphaButton(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaConstraintLayout.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.alpha; import android.content.Context; import android.util.AttributeSet; import androidx.constraintlayout.widget.ConstraintLayout; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaConstraintLayout extends ConstraintLayout implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaConstraintLayout(Context context) { super(context); } public QMUIAlphaConstraintLayout(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaFrameLayout.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.alpha; import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaFrameLayout extends FrameLayout implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaFrameLayout(Context context) { super(context); } public QMUIAlphaFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaImageButton.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.alpha; import android.content.Context; import androidx.appcompat.widget.AppCompatImageButton; import android.util.AttributeSet; public class QMUIAlphaImageButton extends AppCompatImageButton implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaImageButton(Context context) { super(context); } public QMUIAlphaImageButton(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaImageButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaLinearLayout.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.alpha; import android.content.Context; import android.util.AttributeSet; import android.widget.LinearLayout; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaLinearLayout extends LinearLayout implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaLinearLayout(Context context) { super(context); } public QMUIAlphaLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaRelativeLayout.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.alpha; import android.content.Context; import android.util.AttributeSet; import android.widget.RelativeLayout; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaRelativeLayout extends RelativeLayout implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaRelativeLayout(Context context) { super(context); } public QMUIAlphaRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaTextView.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.alpha; import android.content.Context; import android.util.AttributeSet; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; /** * 在 pressed 和 disabled 时改变 View 的透明度 */ public class QMUIAlphaTextView extends QMUISpanTouchFixTextView implements QMUIAlphaViewInf { private QMUIAlphaViewHelper mAlphaViewHelper; public QMUIAlphaTextView(Context context) { super(context); } public QMUIAlphaTextView(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIAlphaTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } @Override protected void onSetPressed(boolean pressed) { super.onSetPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ @Override public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ @Override public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaViewHelper.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.alpha; import androidx.annotation.NonNull; import android.view.View; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIResHelper; import java.lang.ref.WeakReference; public class QMUIAlphaViewHelper { private WeakReference mTarget; /** * 设置是否要在 press 时改变透明度 */ private boolean mChangeAlphaWhenPress = true; /** * 设置是否要在 disabled 时改变透明度 */ private boolean mChangeAlphaWhenDisable = true; private float mNormalAlpha = 1f; private float mPressedAlpha = .5f; private float mDisabledAlpha = .5f; public QMUIAlphaViewHelper(@NonNull View target) { mTarget = new WeakReference<>(target); mPressedAlpha = QMUIResHelper.getAttrFloatValue(target.getContext(), R.attr.qmui_alpha_pressed); mDisabledAlpha = QMUIResHelper.getAttrFloatValue(target.getContext(), R.attr.qmui_alpha_disabled); } public QMUIAlphaViewHelper(@NonNull View target, float pressedAlpha, float disabledAlpha) { mTarget = new WeakReference<>(target); mPressedAlpha = pressedAlpha; mDisabledAlpha = disabledAlpha; } /** * 在 {@link View#setPressed(boolean)} 中调用,通知 helper 更新 * @param current the view to be handled, maybe not equal to target view * @param pressed {@link View#setPressed(boolean)} 中接收到的参数 */ public void onPressedChanged(View current, boolean pressed) { View target = mTarget.get(); if (target == null) { return; } if (current.isEnabled()) { target.setAlpha(mChangeAlphaWhenPress && pressed && current.isClickable() ? mPressedAlpha : mNormalAlpha); } else { if (mChangeAlphaWhenDisable) { target.setAlpha(mDisabledAlpha); } } } /** * 在 {@link View#setEnabled(boolean)} 中调用,通知 helper 更新 * @param current the view to be handled, maybe not equal to target view * @param enabled {@link View#setEnabled(boolean)} 中接收到的参数 */ public void onEnabledChanged(View current, boolean enabled) { View target = mTarget.get(); if (target == null) { return; } float alphaForIsEnable; if (mChangeAlphaWhenDisable) { alphaForIsEnable = enabled ? mNormalAlpha : mDisabledAlpha; } else { alphaForIsEnable = mNormalAlpha; } if (current != target && target.isEnabled() != enabled) { target.setEnabled(enabled); } target.setAlpha(alphaForIsEnable); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { mChangeAlphaWhenPress = changeAlphaWhenPress; } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { mChangeAlphaWhenDisable = changeAlphaWhenDisable; View target = mTarget.get(); if (target != null) { onEnabledChanged(target, target.isEnabled()); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/alpha/QMUIAlphaViewInf.java ================================================ /* * Tencent is pleased to support the open source community by making QMUI_Android available. * * Copyright (C) 2017-2019 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.alpha; /** * 在 pressed 和 disabled 时改变 View 的透明度的接口 */ public interface QMUIAlphaViewInf { /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ void setChangeAlphaWhenPress(boolean changeAlphaWhenPress); /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/exposure/Exposure.kt ================================================ package com.qmuiteam.qmui.exposure import android.view.View enum class ExposureType { first, dataChange, repeat } interface Exposure { fun same(data: Exposure): Boolean fun expose(view: View, type: ExposureType) } class SimpleExposure(val key: Any?, val block: (type: ExposureType) -> Unit) : Exposure { override fun same(data: Exposure): Boolean { return data is SimpleExposure && data.key == key } override fun expose(view: View, type: ExposureType) { block(type) } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureChecker.kt ================================================ package com.qmuiteam.qmui.exposure import android.graphics.Rect import android.view.View import android.view.ViewGroup import com.qmuiteam.qmui.util.QMUIViewHelper import java.util.* private val rect = Rect() interface ExposureChecker { fun canExpose(target: View): Boolean { return target.defaultCanExpose() } fun isExposedInContainer(container: ViewGroup, target: View): Boolean } class FastAreaExposureChecker(val percent: Float) : ExposureChecker { override fun isExposedInContainer(container: ViewGroup, target: View): Boolean { if (target.width <= 0 || target.height <= 0) { return false } QMUIViewHelper.getDescendantRect(container, target, rect) if (rect.left >= container.width || rect.top >= container.height || rect.right <= 0 || rect.bottom <= 0) { return false } if (rect.left < 0) { rect.left = 0 } if (rect.right > container.width) { rect.right = container.width } if (rect.top < 0) { rect.top = 0 } if (rect.bottom > container.height) { rect.bottom = container.height } return (rect.width() * rect.height() * 1f) / (target.width * target.height) >= percent } } class AreaExposureChecker(val percent: Float) : ExposureChecker { override fun isExposedInContainer(container: ViewGroup, target: View): Boolean { if (target.width <= 0 || target.height <= 0) { return false } val hasVisibleArea = QMUIViewHelper.getDescendantVisibleRect(container, target, rect) if (!hasVisibleArea) { return false } return (rect.width() * rect.height() * 1f) / (target.width * target.height) >= percent } } val fastFullExposureChecker = FastAreaExposureChecker(1f) val fullExposureChecker = AreaExposureChecker(1f) val defaultExposureChecker = AreaExposureChecker(0.80f) fun interface CustomExposureTriggerListener { fun doCheck() } class CustomExposureTrigger { private val listeners = mutableListOf() private var isTriggering = false private val pendingActions = LinkedList() fun addListener(listener: CustomExposureTriggerListener) { if (isTriggering) { pendingActions.add(PendingAction(listener, true)) } else { listeners.add(listener) } } fun removeListener(listener: CustomExposureTriggerListener) { if (isTriggering) { pendingActions.add(PendingAction(listener, true)) } else { listeners.remove(listener) } } fun trigger() { isTriggering = true listeners.forEach { it.doCheck() } isTriggering = false var pendingAction = pendingActions.poll() while (pendingAction != null) { if (pendingAction.isDelete) { removeListener(pendingAction.listener) } else { addListener(pendingAction.listener) } pendingAction = pendingActions.poll() } } private class PendingAction( val listener: CustomExposureTriggerListener, val isDelete: Boolean ) } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureContainer.kt ================================================ package com.qmuiteam.qmui.exposure import android.view.View import android.view.ViewGroup interface ExposureContainerProvider { fun provide(view: View): ViewGroup? } object DefaultExposureContainerProvider : ExposureContainerProvider { override fun provide(view: View): ViewGroup? { return view.rootView as? ViewGroup } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEffect.kt ================================================ package com.qmuiteam.qmui.exposure import android.os.SystemClock import android.view.View import android.view.ViewGroup import com.qmuiteam.qmui.R enum class EffectResult { pass, handled, unHandled } interface ExposureEffect { fun doBeforeExpose( target: View, container: ViewGroup, exposure: Exposure, lastExposure: Exposure?, type: ExposureType ): EffectResult fun doAfterUnExpose( target: View, container: ViewGroup, data: Exposure ){ } } class ParentExposedRequestExposureEffect(val parent: ViewGroup) : ExposureEffect { override fun doBeforeExpose( target: View, container: ViewGroup, exposure: Exposure, lastExposure: Exposure?, type: ExposureType ): EffectResult { val isParentConfigSet = parent.getTag(R.id.qmui_exposure_config) as? Boolean ?: false if (!isParentConfigSet) { throw RuntimeException("You should config the exposure on parent($parent) for constraint effect.") } val holder = parent.getTag(R.id.qmui_exposure_holder) as? Runnable if (holder != null) { parent.removeCallbacks(holder) parent.setTag(R.id.qmui_exposure_holder, null) holder.run() } return if(parent.isInExposure()) EffectResult.pass else EffectResult.unHandled } } class RecyclerExposureEffect( val parent: ViewGroup, val safeDuration: Long = 500, val zombieDuration: Long = 2000 ) : ExposureEffect { private val exposureSet = mutableSetOf>() private val zombieSet = mutableSetOf>() override fun doBeforeExpose( target: View, container: ViewGroup, exposure: Exposure, lastExposure: Exposure?, type: ExposureType ): EffectResult { clearZombie() if(type == ExposureType.dataChange){ lastExposure?.also { last -> val exist = exposureSet.find { it.first.same(last) } if(exist == null || exist.second + safeDuration < SystemClock.elapsedRealtime()){ zombieSet.removeAll { it.first.same(last) } zombieSet.add(last to SystemClock.elapsedRealtime()) if(exist != null){ exposureSet.removeAll { it.first.same(last) } } } } } if(exposureSet.find { it.first.same(exposure) } != null){ zombieSet.removeAll { it.first.same(exposure) } return EffectResult.handled } val zombie = zombieSet.find { it.first.same(exposure) } if(zombie != null){ exposureSet.add(exposure to SystemClock.elapsedRealtime()) zombieSet.remove(zombie) return EffectResult.handled } zombieSet.removeAll { it.first.same(exposure) } exposureSet.add(exposure to SystemClock.elapsedRealtime()) return EffectResult.pass } override fun doAfterUnExpose(target: View, container: ViewGroup, data: Exposure) { clearZombie() zombieSet.removeAll { it.first.same(data) } zombieSet.add(data to SystemClock.elapsedRealtime()) exposureSet.removeAll { it.first.same(data) } } private fun clearZombie(){ val iterator = zombieSet.iterator() while (iterator.hasNext()){ val next = iterator.next() if(next.second + zombieDuration < SystemClock.elapsedRealtime()){ iterator.remove() } } } } internal class ExposureEffectList( val container: ViewGroup, val effectList: List ) ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/exposure/ExposureEx.kt ================================================ package com.qmuiteam.qmui.exposure import android.view.View import android.view.ViewGroup import android.view.ViewParent import android.view.ViewTreeObserver import android.widget.AbsListView import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import com.qmuiteam.qmui.R import com.qmuiteam.qmui.kotlin.debounceRun import com.qmuiteam.qmui.widget.tab.QMUIBasicTabSegment /** * Exposure 使用: * 1. 使用场景: * a. 简单使用:simpleExposure(key=xxx, ...) * b. 复杂使用, view 初始化时 registerExposure(...), 渲染数据时 bindExposure(Exposure) * c. 和 RecyclerView/ListView 配合,onBindViewHolder 时:simpleExposure(key=xxx, ...), 或者在 onCreateViewHolder 时 registerExposure(...), * onBindViewHolder 时 bindExposure(Exposure) * d. 有自定义 View 复用逻辑的容器,同 c, 但 ViewGroup 需要调用 setToRecyclerContainer() * e. 如果子 View 需要在父 View 已曝光的前提下才能认为是曝光, 那么父容器需要调用 setSelfExposedWhenDescendantExposed() * * 2. Exposure 类 * 曝光所用的数据类,使用者需要自定义,框架通过 same(Exposure) 判断数据是否变更而觉得是否需要重新曝光, RecyclerView 复用排重也依赖于它 * 框架在满足曝光时触发 expose() 方法 * * 3. 可配置项: * holdTime -> 需要在可视区域停留超过 holdTime 后才算曝光, 默认 600ms * debounceTimeout -> debounce 处理,防止界面多次 layout / scroll 不停触发曝光检查, 默认 400ms * containerProvider -> 在 containerProvider 提供的 ViewGroup 里可视才算曝光,默认是整个界面的 rootView * exposureChecker -> 曝光检查器,默认是可视面积超过自身总面积的 80% 算可见 */ fun View.simpleExposure( holdTime: Long = 600, debounceTimeout: Long = 400, containerProvider: ExposureContainerProvider = DefaultExposureContainerProvider, exposureChecker: ExposureChecker = defaultExposureChecker, key: Any?, doExpose: (type: ExposureType) -> Unit ) { registerExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) bindExposure(SimpleExposure(key) { doExpose(it) }) } fun View.exposure( holdTime: Long = 600, debounceTimeout: Long = 400, containerProvider: ExposureContainerProvider = DefaultExposureContainerProvider, exposureChecker: ExposureChecker = fullExposureChecker, exposure: Exposure ){ registerExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) bindExposure(exposure) } fun View.registerExposure( holdTime: Long = 600, debounceTimeout: Long = 400, containerProvider: ExposureContainerProvider = DefaultExposureContainerProvider, exposureChecker: ExposureChecker = fullExposureChecker ) { setTag(R.id.qmui_exposure_config, true) var attachListener = getTag(R.id.qmui_exposure_register) as? View.OnAttachStateChangeListener if(attachListener != null){ return } attachListener = object : View.OnAttachStateChangeListener { private val onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { checkExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) } private val onScrollListener = ViewTreeObserver.OnScrollChangedListener { checkExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) } private val customTriggerListener = CustomExposureTriggerListener { checkExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) } override fun onViewAttachedToWindow(v: View?) { checkExposure(holdTime, debounceTimeout, containerProvider, exposureChecker) viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) viewTreeObserver.addOnScrollChangedListener(onScrollListener) containerProvider.provide(this@registerExposure)?.let { container -> var exposureCheck = container.getTag(R.id.qmui_exposure_custom_check_trigger) as? CustomExposureTrigger if(exposureCheck == null){ exposureCheck = CustomExposureTrigger().also { container.setTag(R.id.qmui_exposure_custom_check_trigger, it) } } exposureCheck.addListener(customTriggerListener) } } override fun onViewDetachedFromWindow(v: View?) { viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener) viewTreeObserver.removeOnScrollChangedListener(onScrollListener) containerProvider.provide(this@registerExposure)?.let { container -> (container.getTag(R.id.qmui_exposure_custom_check_trigger) as? CustomExposureTrigger)?.removeListener(customTriggerListener) } clearExposureHolder() clearExposureDebounce() doUnExpose() } } setTag(R.id.qmui_exposure_register, attachListener) addOnAttachStateChangeListener(attachListener) if(isAttachedToWindow){ attachListener.onViewAttachedToWindow(this) } } fun View.unregisterExposure(){ setTag(R.id.qmui_exposure_config, false) val attachListener = getTag(R.id.qmui_exposure_register) as? View.OnAttachStateChangeListener if(attachListener != null){ removeOnAttachStateChangeListener(attachListener) attachListener.onViewDetachedFromWindow(this) setTag(R.id.qmui_exposure_register, null) } } fun View.bindExposure(exposure: Exposure) { setTag(R.id.qmui_exposure_data, exposure) } fun View.isInExposure(): Boolean { return getTag(R.id.qmui_exposure_ing) as? Boolean ?: false } fun View.setToRecyclerContainer() { setTag(R.id.qmui_exposure_is_recycler_container, true) } fun ViewGroup.setSelfExposedWhenDescendantExposed(need: Boolean) { if(need){ setTag(R.id.qmui_exposure_parent_expose_request, ParentExposedRequestExposureEffect(this)) }else{ setTag(R.id.qmui_exposure_parent_expose_request, null) } } fun ViewGroup.customConfigRecyclerExposureEffect(effect: RecyclerExposureEffect) { setTag(R.id.qmui_exposure_recycler_collection, effect) } fun View.triggerCustomExposureChecker( containerProvider: ExposureContainerProvider = DefaultExposureContainerProvider ) { if(!isAttachedToWindow){ return } (containerProvider.provide(this)?.getTag(R.id.qmui_exposure_custom_check_trigger) as? CustomExposureTrigger)?.trigger() } fun View.defaultCanExpose(): Boolean { if (!isAttachedToWindow) { return false } if (windowVisibility != View.VISIBLE) { return false } if (visibility != View.VISIBLE) { return false } var p: ViewParent? = parent while (p != null && p is ViewGroup) { if (p.visibility != View.VISIBLE) { return false } p = p.parent } return true } fun View.checkExposure( holdTime: Long = 1000, debounceTimeout: Long = 500, containerProvider: ExposureContainerProvider = DefaultExposureContainerProvider, exposureChecker: ExposureChecker = fullExposureChecker, ) { val holderRunnable = getTag(R.id.qmui_exposure_holder) as? Runnable if (holderRunnable != null) { return } debounceRun(R.id.qmui_exposure_debounce, debounceTimeout) { val container = containerProvider.provide(this) ?: return@debounceRun val isInExposure = isInExposure() if (checkIsExposure(container, exposureChecker)) { if (!isInExposure || checkIsExposureDataChanged()) { val runnable = Runnable { setTag(R.id.qmui_exposure_holder, null) if (checkIsExposure(container, exposureChecker)) { val data = getTag(R.id.qmui_exposure_data) as? Exposure ?: return@Runnable val last = getTag(R.id.qmui_exposure_last_data) as? Exposure val type = when { last == null -> ExposureType.first !last.same(data) -> ExposureType.dataChange else -> ExposureType.repeat } if (doExpose(container, data, last, type)) { setTag(R.id.qmui_exposure_ing, true) setTag(R.id.qmui_exposure_last_data, data) } } }.also { setTag(R.id.qmui_exposure_holder, it) } postDelayed(runnable, holdTime) } } else if (isInExposure) { doUnExpose() } } } private fun View.checkIsExposureDataChanged(): Boolean { val data = getTag(R.id.qmui_exposure_data) as? Exposure ?: return false val last = getTag(R.id.qmui_exposure_last_data) as? Exposure return last == null || !last.same(data) } private fun View.checkIsExposure( container: ViewGroup, exposureChecker: ExposureChecker = fullExposureChecker ): Boolean { if (!exposureChecker.canExpose(this)) { return false } return exposureChecker.isExposedInContainer(container, this) } internal fun View.clearExposureHolder() { (getTag(R.id.qmui_exposure_holder) as? Runnable)?.let { removeCallbacks(it) setTag(R.id.qmui_exposure_holder, null) } } internal fun View.clearExposureDebounce() { (getTag(R.id.qmui_exposure_debounce) as? Runnable)?.let { removeCallbacks(it) setTag(R.id.qmui_exposure_debounce, null) } } internal fun View.doExpose( container: ViewGroup, exposure: Exposure, lastExposure: Exposure?, exposureType: ExposureType ): Boolean { var p = parent as? ViewGroup val exposureList = mutableListOf() var effectResult = EffectResult.pass while (p != null && p != container) { val parentAlready = p.getTag(R.id.qmui_exposure_parent_expose_request) as? ParentExposedRequestExposureEffect if (parentAlready != null) { exposureList.add(parentAlready) val ret = parentAlready.doBeforeExpose(this, container, exposure, lastExposure, exposureType) if (ret != EffectResult.pass) { effectResult = ret break } } if (parent == p && (p is RecyclerView || p is AbsListView || p is QMUIBasicTabSegment || p is ViewPager || p.getTag(R.id.qmui_exposure_is_recycler_container) == true) ) { var recyclerEffect = p.getTag(R.id.qmui_exposure_recycler_collection) as? RecyclerExposureEffect if (recyclerEffect == null) { recyclerEffect = RecyclerExposureEffect(p) p.setTag(R.id.qmui_exposure_recycler_collection, recyclerEffect) } exposureList.add(recyclerEffect) val ret = recyclerEffect.doBeforeExpose(this, container, exposure, lastExposure, exposureType) if (ret != EffectResult.pass) { effectResult = ret break } } val customEffect = p.getTag(R.id.qmui_exposure_custom_effect) as? ExposureEffect if (customEffect != null) { exposureList.add(customEffect) val ret = customEffect.doBeforeExpose(this, container, exposure, lastExposure, exposureType) if (ret != EffectResult.pass) { effectResult = ret break } } p = p.parent as? ViewGroup } setTag(R.id.qmui_exposure_effect_list, ExposureEffectList(container, exposureList)) if (effectResult == EffectResult.pass) { exposure.expose(this, exposureType) effectResult = EffectResult.handled } return effectResult == EffectResult.handled } internal fun View.doUnExpose() { if (isInExposure()) { setTag(R.id.qmui_exposure_ing, false) val exposure = getTag(R.id.qmui_exposure_data) as? Exposure ?: return (getTag(R.id.qmui_exposure_effect_list) as? ExposureEffectList)?.let { it.effectList.forEach { effect -> effect.doAfterUnExpose(this, it.container, exposure) } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/kotlin/DimenKt.kt ================================================ package com.qmuiteam.qmui.kotlin import android.content.Context import android.view.View import androidx.annotation.DimenRes import androidx.fragment.app.Fragment fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt() fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt() fun Context.sp(value: Int): Int = (value * resources.displayMetrics.scaledDensity).toInt() fun Context.sp(value: Float): Int = (value * resources.displayMetrics.scaledDensity).toInt() fun Context.px2dp(px: Int): Float = px.toFloat() / resources.displayMetrics.density fun Context.px2sp(px: Int): Float = px.toFloat() / resources.displayMetrics.scaledDensity fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource) fun View.dip(value: Int): Int = context.dip(value) fun View.dip(value: Float): Int = context.dip(value) fun View.sp(value: Int): Int = context.sp(value) fun View.sp(value: Float): Int = context.sp(value) fun View.px2dp(px: Int): Float = context.px2dp(px) fun View.px2sp(px: Int): Float = context.px2sp(px) fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource) // must be called after attached. fun Fragment.dip(value: Int): Int = context!!.dip(value) fun Fragment.dip(value: Float): Int = context!!.dip(value) fun Fragment.sp(value: Int): Int = context!!.sp(value) fun Fragment.sp(value: Float): Int = context!!.sp(value) fun Fragment.px2dp(px: Int): Float = context!!.px2dp(px) fun Fragment.px2sp(px: Int): Float = context!!.px2sp(px) fun Fragment.dimen(@DimenRes resource: Int): Int = context!!.dimen(resource) ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/kotlin/LayoutParamKt.kt ================================================ package com.qmuiteam.qmui.kotlin import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout val matchParent: Int = ViewGroup.LayoutParams.MATCH_PARENT val wrapContent: Int = ViewGroup.LayoutParams.WRAP_CONTENT val matchConstraint: Int = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT val constraintParentId = ConstraintLayout.LayoutParams.PARENT_ID fun ConstraintLayout.LayoutParams.alignParent4(){ leftToLeft = constraintParentId rightToRight = constraintParentId topToTop = constraintParentId bottomToBottom = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentHor(){ leftToLeft = constraintParentId rightToRight = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentVer(){ topToTop = constraintParentId bottomToBottom = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentLeftTop(){ topToTop = constraintParentId leftToLeft = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentLeftBottom(){ bottomToBottom = constraintParentId leftToLeft = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentRightTop(){ topToTop = constraintParentId rightToRight = constraintParentId } fun ConstraintLayout.LayoutParams.alignParentRightBottom(){ bottomToBottom = constraintParentId rightToRight = constraintParentId } fun ConstraintLayout.LayoutParams.alignView4(id: Int){ leftToLeft = id rightToRight = id topToTop = id bottomToBottom = id } fun ConstraintLayout.LayoutParams.alignViewHor(id: Int){ leftToLeft = id rightToRight = id } fun ConstraintLayout.LayoutParams.alignViewVer(id: Int){ topToTop = id bottomToBottom = id } fun ConstraintLayout.LayoutParams.alignViewLeftTop(id: Int){ topToTop = id leftToLeft = id } fun ConstraintLayout.LayoutParams.alignViewLeftBottom(id: Int){ bottomToBottom = id leftToLeft = id } fun ConstraintLayout.LayoutParams.alignViewRightTop(id: Int){ topToTop = id rightToRight = id } fun ConstraintLayout.LayoutParams.alignViewRightBottom(id: Int){ bottomToBottom = id rightToRight = id } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/kotlin/ViewKt.kt ================================================ package com.qmuiteam.qmui.kotlin import android.os.SystemClock import android.view.View import com.qmuiteam.qmui.R import com.qmuiteam.qmui.skin.QMUISkinHelper import com.qmuiteam.qmui.skin.QMUISkinValueBuilder fun View.throttleRun( id: Int, timeout: Long, block: () -> Unit ){ val exit = getTag(id) as? Runnable if(exit != null){ return } val nextThrottle = Runnable { setTag(id, null) block() }.also { setTag(id, it) } postDelayed(nextThrottle, timeout) } fun View.debounceRun( id: Int, timeout: Long, block: () -> Unit ){ val exit = getTag(id) as? Runnable if(exit != null){ removeCallbacks(exit) postDelayed(exit, timeout) return } val nextThrottle = Runnable { setTag(id, null) block() }.also { setTag(id, it) } postDelayed(nextThrottle, timeout) } fun throttleClick(wait: Long = 200, block: ((View) -> Unit)): View.OnClickListener { return View.OnClickListener { v -> val current = SystemClock.uptimeMillis() val lastClickTime = (v.getTag(R.id.qmui_click_timestamp) as? Long) ?: 0 if (current - lastClickTime > wait) { v.setTag(R.id.qmui_click_timestamp, current) block(v) } } } fun debounceClick(wait: Long = 200, block: ((View) -> Unit)): View.OnClickListener { return View.OnClickListener { v -> var action = (v.getTag(R.id.qmui_click_debounce_action) as? DebounceAction) if(action == null){ action = DebounceAction(v, block) v.setTag(R.id.qmui_click_debounce_action, action) }else{ action.block = block } v.removeCallbacks(action) v.postDelayed(action, wait) } } class DebounceAction(val view: View, var block: ((View) -> Unit)): Runnable { override fun run() { if(view.isAttachedToWindow){ block(view) } } } fun View.onClick(wait: Long = 200, block: ((View) -> Unit)) { setOnClickListener(throttleClick(wait, block)) } fun View.onDebounceClick(wait: Long = 200, block: ((View) -> Unit)) { setOnClickListener(debounceClick(wait, block)) } fun View.skin(increment: Boolean = false, block:(QMUISkinValueBuilder.() -> Unit)){ val builder = QMUISkinValueBuilder.acquire() if(increment){ val oldSkinValue = getTag(R.id.qmui_skin_value) if(oldSkinValue is String){ builder.convertFrom(oldSkinValue) } } builder.block() QMUISkinHelper.setSkinValue(this, builder) builder.release() } fun View.clearSkin(){ QMUISkinHelper.setSkinValue(this, "") } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/IQMUILayout.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.layout; import android.view.View; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import androidx.annotation.ColorInt; import androidx.annotation.IntDef; /** * Created by cgspine on 2018/3/23. */ public interface IQMUILayout { int HIDE_RADIUS_SIDE_NONE = 0; int HIDE_RADIUS_SIDE_TOP = 1; int HIDE_RADIUS_SIDE_RIGHT = 2; int HIDE_RADIUS_SIDE_BOTTOM = 3; int HIDE_RADIUS_SIDE_LEFT = 4; @IntDef(value = { HIDE_RADIUS_SIDE_NONE, HIDE_RADIUS_SIDE_TOP, HIDE_RADIUS_SIDE_RIGHT, HIDE_RADIUS_SIDE_BOTTOM, HIDE_RADIUS_SIDE_LEFT}) @Retention(RetentionPolicy.SOURCE) @interface HideRadiusSide { } /** * limit the width of a layout * * @param widthLimit * @return */ boolean setWidthLimit(int widthLimit); /** * limit the height of a layout * * @param heightLimit * @return */ boolean setHeightLimit(int heightLimit); /** * use the shadow elevation from the theme */ void setUseThemeGeneralShadowElevation(); /** * determine if the outline contain the padding area, usually false * * @param outlineExcludePadding */ void setOutlineExcludePadding(boolean outlineExcludePadding); /** * See {@link android.view.View#setElevation(float)} * * @param elevation */ void setShadowElevation(int elevation); /** * See {@link View#getElevation()} * * @return */ int getShadowElevation(); /** * set the outline alpha, which will change the shadow * * @param shadowAlpha */ void setShadowAlpha(float shadowAlpha); /** * get the outline alpha we set * * @return */ float getShadowAlpha(); /** * @param shadowColor opaque color * @return */ void setShadowColor(int shadowColor); /** * @return opaque color */ int getShadowColor(); /** * set the layout radius * * @param radius */ void setRadius(int radius); /** * set the layout radius with one or none side been hidden * * @param radius * @param hideRadiusSide */ void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide); /** * get the layout radius * * @return */ int getRadius(); /** * inset the outline if needed * * @param left * @param top * @param right * @param bottom */ void setOutlineInset(int left, int top, int right, int bottom); /** * the shadow elevation only work after L, so we provide a downgrading compatible solutions for android 4.x * usually we use border, but the border may be redundant for android L+. so will not show border default, * if your designer like the border exists with shadow, you can call setShowBorderOnlyBeforeL(false) * * @param showBorderOnlyBeforeL */ void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL); /** * in some case, we maybe hope the layout only have radius in one side. * but there is no convenient way to write the code like canvas.drawPath, * so we take another way that hide one radius side * * @param hideRadiusSide */ void setHideRadiusSide(@HideRadiusSide int hideRadiusSide); /** * get the side that we have hidden the radius * * @return */ int getHideRadiusSide(); /** * this method will determine the radius and shadow. * * @param radius * @param shadowElevation * @param shadowAlpha */ void setRadiusAndShadow(int radius, int shadowElevation, float shadowAlpha); /** * this method will determine the radius and shadow with one or none side be hidden * * @param radius * @param hideRadiusSide * @param shadowElevation * @param shadowAlpha */ void setRadiusAndShadow(int radius, @HideRadiusSide int hideRadiusSide, int shadowElevation, float shadowAlpha); /** * this method will determine the radius and shadow (support shadowColor if after android 9)with one or none side be hidden * * @param radius * @param hideRadiusSide * @param shadowElevation * @param shadowColor * @param shadowAlpha */ void setRadiusAndShadow(int radius, @HideRadiusSide int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha); /** * border color, if you don not set it, the layout will not draw the border * * @param borderColor */ void setBorderColor(@ColorInt int borderColor); /** * border width, default is 1px, usually no need to set * * @param borderWidth */ void setBorderWidth(int borderWidth); /** * config the top divider * * @param topInsetLeft * @param topInsetRight * @param topDividerHeight * @param topDividerColor */ void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor); /** * config the bottom divider * * @param bottomInsetLeft * @param bottomInsetRight * @param bottomDividerHeight * @param bottomDividerColor */ void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor); /** * config the left divider * * @param leftInsetTop * @param leftInsetBottom * @param leftDividerWidth * @param leftDividerColor */ void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor); /** * config the right divider * * @param rightInsetTop * @param rightInsetBottom * @param rightDividerWidth * @param rightDividerColor */ void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor); /** * show top divider, and hide others * * @param topInsetLeft * @param topInsetRight * @param topDividerHeight * @param topDividerColor */ void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor); /** * show bottom divider, and hide others * * @param bottomInsetLeft * @param bottomInsetRight * @param bottomDividerHeight * @param bottomDividerColor */ void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor); /** * show left divider, and hide others * * @param leftInsetTop * @param leftInsetBottom * @param leftDividerWidth * @param leftDividerColor */ void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor); /** * show right divider, and hide others * * @param rightInsetTop * @param rightInsetBottom * @param rightDividerWidth * @param rightDividerColor */ void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor); /** * after config the border, sometimes we need change the alpha of divider with animation, * so we provide a method to individually change the alpha * * @param dividerAlpha [0, 255] */ void setTopDividerAlpha(int dividerAlpha); /** * @param dividerAlpha [0, 255] */ void setBottomDividerAlpha(int dividerAlpha); /** * @param dividerAlpha [0, 255] */ void setLeftDividerAlpha(int dividerAlpha); /** * @param dividerAlpha [0, 255] */ void setRightDividerAlpha(int dividerAlpha); /** * only available before android L * * @param color */ void setOuterNormalColor(int color); /** * update left separator color * * @param color */ void updateLeftSeparatorColor(int color); /** * update right separator color * * @param color */ void updateRightSeparatorColor(int color); /** * update top separator color * * @param color */ void updateTopSeparatorColor(int color); /** * update bottom separator color * * @param color */ void updateBottomSeparatorColor(int color); boolean hasTopSeparator(); boolean hasRightSeparator(); boolean hasLeftSeparator(); boolean hasBottomSeparator(); boolean hasBorder(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIButton.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.layout; import android.content.Context; import android.graphics.Canvas; import androidx.annotation.ColorInt; import android.util.AttributeSet; import com.qmuiteam.qmui.alpha.QMUIAlphaButton; /** * Created by cgspine on 2018/3/1. */ public class QMUIButton extends QMUIAlphaButton implements IQMUILayout { private QMUILayoutHelper mLayoutHelper; public QMUIButton(Context context) { super(context); init(context, null, 0); } public QMUIButton(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenDisable(false); setChangeAlphaWhenPress(false); } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); invalidate(); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIConstraintLayout.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.layout; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import androidx.annotation.ColorInt; import com.qmuiteam.qmui.alpha.QMUIAlphaConstraintLayout; /** * @author cginechen * @date 2017-03-10 */ public class QMUIConstraintLayout extends QMUIAlphaConstraintLayout implements IQMUILayout { private QMUILayoutHelper mLayoutHelper; public QMUIConstraintLayout(Context context) { super(context); init(context, null, 0); } public QMUIConstraintLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenPress(false); setChangeAlphaWhenDisable(false); } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void dispatchDraw(Canvas canvas) { try { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); }catch (Throwable ignore){ // unreasonable crash } } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIFrameLayout.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.layout; import android.content.Context; import android.graphics.Canvas; import androidx.annotation.ColorInt; import android.util.AttributeSet; import com.qmuiteam.qmui.alpha.QMUIAlphaFrameLayout; /** * @author cginechen * @date 2017-03-10 */ public class QMUIFrameLayout extends QMUIAlphaFrameLayout implements IQMUILayout { private QMUILayoutHelper mLayoutHelper; public QMUIFrameLayout(Context context) { super(context); init(context, null, 0); } public QMUIFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenDisable(false); setChangeAlphaWhenPress(false); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILayoutHelper.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.layout; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIResHelper; import java.lang.ref.WeakReference; import androidx.annotation.ColorInt; import androidx.core.content.ContextCompat; /** * @author cginechen * @date 2017-03-10 */ public class QMUILayoutHelper implements IQMUILayout { public static final int RADIUS_OF_HALF_VIEW_HEIGHT = -1; public static final int RADIUS_OF_HALF_VIEW_WIDTH = -2; private Context mContext; // size private int mWidthLimit = 0; private int mHeightLimit = 0; private int mWidthMini = 0; private int mHeightMini = 0; // divider private int mTopDividerHeight = 0; private int mTopDividerInsetLeft = 0; private int mTopDividerInsetRight = 0; private int mTopDividerColor; private int mTopDividerAlpha = 255; private int mBottomDividerHeight = 0; private int mBottomDividerInsetLeft = 0; private int mBottomDividerInsetRight = 0; private int mBottomDividerColor; private int mBottomDividerAlpha = 255; private int mLeftDividerWidth = 0; private int mLeftDividerInsetTop = 0; private int mLeftDividerInsetBottom = 0; private int mLeftDividerColor; private int mLeftDividerAlpha = 255; private int mRightDividerWidth = 0; private int mRightDividerInsetTop = 0; private int mRightDividerInsetBottom = 0; private int mRightDividerColor; private int mRightDividerAlpha = 255; private Paint mDividerPaint; // round private Paint mClipPaint; private PorterDuffXfermode mMode; private int mRadius; private @IQMUILayout.HideRadiusSide int mHideRadiusSide = HIDE_RADIUS_SIDE_NONE; private float[] mRadiusArray; private boolean mShouldUseRadiusArray; private RectF mBorderRect; private int mBorderColor = 0; private int mBorderWidth = 1; private int mOuterNormalColor = 0; private WeakReference mOwner; private boolean mIsOutlineExcludePadding = false; private Path mPath = new Path(); // shadow private boolean mIsShowBorderOnlyBeforeL = true; private int mShadowElevation = 0; private float mShadowAlpha; private int mShadowColor = Color.BLACK; // outline inset private int mOutlineInsetLeft = 0; private int mOutlineInsetRight = 0; private int mOutlineInsetTop = 0; private int mOutlineInsetBottom = 0; public QMUILayoutHelper(Context context, AttributeSet attrs, int defAttr, View owner) { this(context, attrs, defAttr, 0, owner); } public QMUILayoutHelper(Context context, AttributeSet attrs, int defAttr, int defStyleRes, View owner) { mContext = context; mOwner = new WeakReference<>(owner); mBottomDividerColor = mTopDividerColor = ContextCompat.getColor(context, R.color.qmui_config_color_separator); mMode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); mClipPaint = new Paint(); mClipPaint.setAntiAlias(true); mShadowAlpha = QMUIResHelper.getAttrFloatValue(context, R.attr.qmui_general_shadow_alpha); mBorderRect = new RectF(); int radius = 0, shadow = 0; boolean useThemeGeneralShadowElevation = false; if (null != attrs || defAttr != 0 || defStyleRes != 0) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QMUILayout, defAttr, defStyleRes); int count = ta.getIndexCount(); for (int i = 0; i < count; ++i) { int index = ta.getIndex(i); if (index == R.styleable.QMUILayout_android_maxWidth) { mWidthLimit = ta.getDimensionPixelSize(index, mWidthLimit); } else if (index == R.styleable.QMUILayout_android_maxHeight) { mHeightLimit = ta.getDimensionPixelSize(index, mHeightLimit); } else if (index == R.styleable.QMUILayout_android_minWidth) { mWidthMini = ta.getDimensionPixelSize(index, mWidthMini); } else if (index == R.styleable.QMUILayout_android_minHeight) { mHeightMini = ta.getDimensionPixelSize(index, mHeightMini); } else if (index == R.styleable.QMUILayout_qmui_topDividerColor) { mTopDividerColor = ta.getColor(index, mTopDividerColor); } else if (index == R.styleable.QMUILayout_qmui_topDividerHeight) { mTopDividerHeight = ta.getDimensionPixelSize(index, mTopDividerHeight); } else if (index == R.styleable.QMUILayout_qmui_topDividerInsetLeft) { mTopDividerInsetLeft = ta.getDimensionPixelSize(index, mTopDividerInsetLeft); } else if (index == R.styleable.QMUILayout_qmui_topDividerInsetRight) { mTopDividerInsetRight = ta.getDimensionPixelSize(index, mTopDividerInsetRight); } else if (index == R.styleable.QMUILayout_qmui_bottomDividerColor) { mBottomDividerColor = ta.getColor(index, mBottomDividerColor); } else if (index == R.styleable.QMUILayout_qmui_bottomDividerHeight) { mBottomDividerHeight = ta.getDimensionPixelSize(index, mBottomDividerHeight); } else if (index == R.styleable.QMUILayout_qmui_bottomDividerInsetLeft) { mBottomDividerInsetLeft = ta.getDimensionPixelSize(index, mBottomDividerInsetLeft); } else if (index == R.styleable.QMUILayout_qmui_bottomDividerInsetRight) { mBottomDividerInsetRight = ta.getDimensionPixelSize(index, mBottomDividerInsetRight); } else if (index == R.styleable.QMUILayout_qmui_leftDividerColor) { mLeftDividerColor = ta.getColor(index, mLeftDividerColor); } else if (index == R.styleable.QMUILayout_qmui_leftDividerWidth) { mLeftDividerWidth = ta.getDimensionPixelSize(index, mLeftDividerWidth); } else if (index == R.styleable.QMUILayout_qmui_leftDividerInsetTop) { mLeftDividerInsetTop = ta.getDimensionPixelSize(index, mLeftDividerInsetTop); } else if (index == R.styleable.QMUILayout_qmui_leftDividerInsetBottom) { mLeftDividerInsetBottom = ta.getDimensionPixelSize(index, mLeftDividerInsetBottom); } else if (index == R.styleable.QMUILayout_qmui_rightDividerColor) { mRightDividerColor = ta.getColor(index, mRightDividerColor); } else if (index == R.styleable.QMUILayout_qmui_rightDividerWidth) { mRightDividerWidth = ta.getDimensionPixelSize(index, mRightDividerWidth); } else if (index == R.styleable.QMUILayout_qmui_rightDividerInsetTop) { mRightDividerInsetTop = ta.getDimensionPixelSize(index, mRightDividerInsetTop); } else if (index == R.styleable.QMUILayout_qmui_rightDividerInsetBottom) { mRightDividerInsetBottom = ta.getDimensionPixelSize(index, mRightDividerInsetBottom); } else if (index == R.styleable.QMUILayout_qmui_borderColor) { mBorderColor = ta.getColor(index, mBorderColor); } else if (index == R.styleable.QMUILayout_qmui_borderWidth) { mBorderWidth = ta.getDimensionPixelSize(index, mBorderWidth); } else if (index == R.styleable.QMUILayout_qmui_radius) { radius = ta.getDimensionPixelSize(index, 0); } else if (index == R.styleable.QMUILayout_qmui_outerNormalColor) { mOuterNormalColor = ta.getColor(index, mOuterNormalColor); } else if (index == R.styleable.QMUILayout_qmui_hideRadiusSide) { mHideRadiusSide = ta.getInt(index, mHideRadiusSide); } else if (index == R.styleable.QMUILayout_qmui_showBorderOnlyBeforeL) { mIsShowBorderOnlyBeforeL = ta.getBoolean(index, mIsShowBorderOnlyBeforeL); } else if (index == R.styleable.QMUILayout_qmui_shadowElevation) { shadow = ta.getDimensionPixelSize(index, shadow); } else if (index == R.styleable.QMUILayout_qmui_shadowAlpha) { mShadowAlpha = ta.getFloat(index, mShadowAlpha); } else if (index == R.styleable.QMUILayout_qmui_useThemeGeneralShadowElevation) { useThemeGeneralShadowElevation = ta.getBoolean(index, false); } else if (index == R.styleable.QMUILayout_qmui_outlineInsetLeft) { mOutlineInsetLeft = ta.getDimensionPixelSize(index, 0); } else if (index == R.styleable.QMUILayout_qmui_outlineInsetRight) { mOutlineInsetRight = ta.getDimensionPixelSize(index, 0); } else if (index == R.styleable.QMUILayout_qmui_outlineInsetTop) { mOutlineInsetTop = ta.getDimensionPixelSize(index, 0); } else if (index == R.styleable.QMUILayout_qmui_outlineInsetBottom) { mOutlineInsetBottom = ta.getDimensionPixelSize(index, 0); } else if (index == R.styleable.QMUILayout_qmui_outlineExcludePadding) { mIsOutlineExcludePadding = ta.getBoolean(index, false); } } ta.recycle(); } if (shadow == 0 && useThemeGeneralShadowElevation) { shadow = QMUIResHelper.getAttrDimen(context, R.attr.qmui_general_shadow_elevation); } setRadiusAndShadow(radius, mHideRadiusSide, shadow, mShadowAlpha); } @Override public void setUseThemeGeneralShadowElevation() { mShadowElevation = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_general_shadow_elevation); setRadiusAndShadow(mRadius, mHideRadiusSide, mShadowElevation, mShadowAlpha); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { if (useFeature()) { View owner = mOwner.get(); if (owner == null) { return; } mIsOutlineExcludePadding = outlineExcludePadding; owner.invalidateOutline(); } } @Override public boolean setWidthLimit(int widthLimit) { if (mWidthLimit != widthLimit) { mWidthLimit = widthLimit; return true; } return false; } @Override public boolean setHeightLimit(int heightLimit) { if (mHeightLimit != heightLimit) { mHeightLimit = heightLimit; return true; } return false; } @Override public void updateLeftSeparatorColor(int color) { if (mLeftDividerColor != color) { mLeftDividerColor = color; invalidate(); } } @Override public void updateBottomSeparatorColor(int color) { if (mBottomDividerColor != color) { mBottomDividerColor = color; invalidate(); } } @Override public void updateTopSeparatorColor(int color) { if (mTopDividerColor != color) { mTopDividerColor = color; invalidate(); } } @Override public void updateRightSeparatorColor(int color) { if (mRightDividerColor != color) { mRightDividerColor = color; invalidate(); } } @Override public int getShadowElevation() { return mShadowElevation; } @Override public float getShadowAlpha() { return mShadowAlpha; } @Override public int getShadowColor() { return mShadowColor; } @Override public void setOutlineInset(int left, int top, int right, int bottom) { if (useFeature()) { View owner = mOwner.get(); if (owner == null) { return; } mOutlineInsetLeft = left; mOutlineInsetRight = right; mOutlineInsetTop = top; mOutlineInsetBottom = bottom; owner.invalidateOutline(); } } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mIsShowBorderOnlyBeforeL = showBorderOnlyBeforeL; invalidate(); } @Override public void setShadowElevation(int elevation) { if (mShadowElevation == elevation) { return; } mShadowElevation = elevation; invalidateOutline(); } @Override public void setShadowAlpha(float shadowAlpha) { if (mShadowAlpha == shadowAlpha) { return; } mShadowAlpha = shadowAlpha; invalidateOutline(); } @Override public void setShadowColor(int shadowColor) { if (mShadowColor == shadowColor) { return; } mShadowColor = shadowColor; setShadowColorInner(mShadowColor); } private void setShadowColorInner(int shadowColor) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { View owner = mOwner.get(); if (owner == null) { return; } owner.setOutlineAmbientShadowColor(shadowColor); owner.setOutlineSpotShadowColor(shadowColor); } } private void invalidateOutline() { if (useFeature()) { View owner = mOwner.get(); if (owner == null) { return; } if (mShadowElevation == 0) { owner.setElevation(0); } else { owner.setElevation(mShadowElevation); } owner.invalidateOutline(); } } private void invalidate() { View owner = mOwner.get(); if (owner == null) { return; } owner.invalidate(); } @Override public void setHideRadiusSide(@HideRadiusSide int hideRadiusSide) { if (mHideRadiusSide == hideRadiusSide) { return; } setRadiusAndShadow(mRadius, hideRadiusSide, mShadowElevation, mShadowAlpha); } @Override public int getHideRadiusSide() { return mHideRadiusSide; } @Override public void setRadius(int radius) { if (mRadius != radius) { setRadiusAndShadow(radius, mShadowElevation, mShadowAlpha); } } @Override public void setRadius(int radius, @IQMUILayout.HideRadiusSide int hideRadiusSide) { if (mRadius == radius && hideRadiusSide == mHideRadiusSide) { return; } setRadiusAndShadow(radius, hideRadiusSide, mShadowElevation, mShadowAlpha); } @Override public int getRadius() { return mRadius; } @Override public void setRadiusAndShadow(int radius, int shadowElevation, float shadowAlpha) { setRadiusAndShadow(radius, mHideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @IQMUILayout.HideRadiusSide int hideRadiusSide, int shadowElevation, float shadowAlpha) { setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, mShadowColor, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { final View owner = mOwner.get(); if (owner == null) { return; } mRadius = radius; mHideRadiusSide = hideRadiusSide; mShouldUseRadiusArray = isRadiusWithSideHidden(); mShadowElevation = shadowElevation; mShadowAlpha = shadowAlpha; mShadowColor = shadowColor; if (useFeature()) { if (mShadowElevation == 0 || mShouldUseRadiusArray) { owner.setElevation(0); } else { owner.setElevation(mShadowElevation); } setShadowColorInner(mShadowColor); owner.setOutlineProvider(new ViewOutlineProvider() { @Override @TargetApi(21) public void getOutline(View view, Outline outline) { int w = view.getWidth(), h = view.getHeight(); if (w == 0 || h == 0) { return; } float radius = getRealRadius(); int min = Math.min(w, h); if (radius * 2 > min) { // 解决 OnePlus 3T 8.0 上显示变形 radius = min / 2F; } if (mShouldUseRadiusArray) { int left = 0, top = 0, right = w, bottom = h; if (mHideRadiusSide == HIDE_RADIUS_SIDE_LEFT) { left -= radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_TOP) { top -= radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_RIGHT) { right += radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_BOTTOM) { bottom += radius; } outline.setRoundRect(left, top, right, bottom, radius); return; } int top = mOutlineInsetTop, bottom = Math.max(top + 1, h - mOutlineInsetBottom), left = mOutlineInsetLeft, right = w - mOutlineInsetRight; if (mIsOutlineExcludePadding) { left += view.getPaddingLeft(); top += view.getPaddingTop(); right = Math.max(left + 1, right - view.getPaddingRight()); bottom = Math.max(top + 1, bottom - view.getPaddingBottom()); } float shadowAlpha = mShadowAlpha; if (mShadowElevation == 0) { // outline.setAlpha will work even if shadowElevation == 0 shadowAlpha = 1f; } outline.setAlpha(shadowAlpha); if (radius <= 0) { outline.setRect(left, top, right, bottom); } else { outline.setRoundRect(left, top, right, bottom, radius); } } }); owner.setClipToOutline(mRadius == RADIUS_OF_HALF_VIEW_WIDTH || mRadius == RADIUS_OF_HALF_VIEW_HEIGHT || mRadius > 0); } owner.invalidate(); } /** * 有radius, 但是有一边不显示radius。 * * @return */ public boolean isRadiusWithSideHidden() { return (mRadius == RADIUS_OF_HALF_VIEW_HEIGHT || mRadius == RADIUS_OF_HALF_VIEW_WIDTH || mRadius > 0) && mHideRadiusSide != HIDE_RADIUS_SIDE_NONE; } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mTopDividerInsetLeft = topInsetLeft; mTopDividerInsetRight = topInsetRight; mTopDividerHeight = topDividerHeight; mTopDividerColor = topDividerColor; } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mBottomDividerInsetLeft = bottomInsetLeft; mBottomDividerInsetRight = bottomInsetRight; mBottomDividerColor = bottomDividerColor; mBottomDividerHeight = bottomDividerHeight; } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLeftDividerInsetTop = leftInsetTop; mLeftDividerInsetBottom = leftInsetBottom; mLeftDividerWidth = leftDividerWidth; mLeftDividerColor = leftDividerColor; } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mRightDividerInsetTop = rightInsetTop; mRightDividerInsetBottom = rightInsetBottom; mRightDividerWidth = rightDividerWidth; mRightDividerColor = rightDividerColor; } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); mLeftDividerWidth = 0; mRightDividerWidth = 0; mBottomDividerHeight = 0; } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); mLeftDividerWidth = 0; mRightDividerWidth = 0; mTopDividerHeight = 0; } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); mRightDividerWidth = 0; mTopDividerHeight = 0; mBottomDividerHeight = 0; } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); mLeftDividerWidth = 0; mTopDividerHeight = 0; mBottomDividerHeight = 0; } @Override public void setTopDividerAlpha(int dividerAlpha) { mTopDividerAlpha = dividerAlpha; } @Override public void setBottomDividerAlpha(int dividerAlpha) { mBottomDividerAlpha = dividerAlpha; } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLeftDividerAlpha = dividerAlpha; } @Override public void setRightDividerAlpha(int dividerAlpha) { mRightDividerAlpha = dividerAlpha; } public int handleMiniWidth(int widthMeasureSpec, int measuredWidth) { if (View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY && measuredWidth < mWidthMini) { return View.MeasureSpec.makeMeasureSpec(mWidthMini, View.MeasureSpec.EXACTLY); } return widthMeasureSpec; } public int handleMiniHeight(int heightMeasureSpec, int measuredHeight) { if (View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY && measuredHeight < mHeightMini) { return View.MeasureSpec.makeMeasureSpec(mHeightMini, View.MeasureSpec.EXACTLY); } return heightMeasureSpec; } public int getMeasuredWidthSpec(int widthMeasureSpec) { if (mWidthLimit > 0) { int size = View.MeasureSpec.getSize(widthMeasureSpec); if (size > mWidthLimit) { int mode = View.MeasureSpec.getMode(widthMeasureSpec); if (mode == View.MeasureSpec.AT_MOST) { widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mWidthLimit, View.MeasureSpec.AT_MOST); } else { widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mWidthLimit, View.MeasureSpec.EXACTLY); } } } return widthMeasureSpec; } public int getMeasuredHeightSpec(int heightMeasureSpec) { if (mHeightLimit > 0) { int size = View.MeasureSpec.getSize(heightMeasureSpec); if (size > mHeightLimit) { int mode = View.MeasureSpec.getMode(heightMeasureSpec); if (mode == View.MeasureSpec.AT_MOST) { heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mWidthLimit, View.MeasureSpec.AT_MOST); } else { heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mWidthLimit, View.MeasureSpec.EXACTLY); } } } return heightMeasureSpec; } @Override public void setBorderColor(@ColorInt int borderColor) { mBorderColor = borderColor; } @Override public void setBorderWidth(int borderWidth) { mBorderWidth = borderWidth; } @Override public void setOuterNormalColor(int color) { mOuterNormalColor = color; View owner = mOwner.get(); if (owner != null) { owner.invalidate(); } } @Override public boolean hasTopSeparator() { return mTopDividerHeight > 0; } @Override public boolean hasRightSeparator() { return mRightDividerWidth > 0; } @Override public boolean hasBottomSeparator() { return mBottomDividerHeight > 0; } @Override public boolean hasLeftSeparator() { return mLeftDividerWidth > 0; } @Override public boolean hasBorder() { return mBorderWidth > 0; } public void drawDividers(Canvas canvas, int w, int h) { View owner = mOwner.get(); if(owner == null){ return; } if (mDividerPaint == null && (mTopDividerHeight > 0 || mBottomDividerHeight > 0 || mLeftDividerWidth > 0 || mRightDividerWidth > 0)) { mDividerPaint = new Paint(); } canvas.save(); canvas.translate(owner.getScrollX(), owner.getScrollY()); if (mTopDividerHeight > 0) { mDividerPaint.setStrokeWidth(mTopDividerHeight); mDividerPaint.setColor(mTopDividerColor); if (mTopDividerAlpha < 255) { mDividerPaint.setAlpha(mTopDividerAlpha); } float y = mTopDividerHeight / 2f; canvas.drawLine(mTopDividerInsetLeft, y, w - mTopDividerInsetRight, y, mDividerPaint); } if (mBottomDividerHeight > 0) { mDividerPaint.setStrokeWidth(mBottomDividerHeight); mDividerPaint.setColor(mBottomDividerColor); if (mBottomDividerAlpha < 255) { mDividerPaint.setAlpha(mBottomDividerAlpha); } float y = (float) Math.floor(h - mBottomDividerHeight / 2f); canvas.drawLine(mBottomDividerInsetLeft, y, w - mBottomDividerInsetRight, y, mDividerPaint); } if (mLeftDividerWidth > 0) { mDividerPaint.setStrokeWidth(mLeftDividerWidth); mDividerPaint.setColor(mLeftDividerColor); if (mLeftDividerAlpha < 255) { mDividerPaint.setAlpha(mLeftDividerAlpha); } float x = mLeftDividerWidth / 2f; canvas.drawLine(x, mLeftDividerInsetTop, x, h - mLeftDividerInsetBottom, mDividerPaint); } if (mRightDividerWidth > 0) { mDividerPaint.setStrokeWidth(mRightDividerWidth); mDividerPaint.setColor(mRightDividerColor); if (mRightDividerAlpha < 255) { mDividerPaint.setAlpha(mRightDividerAlpha); } float x = (float) Math.floor(w - mRightDividerWidth / 2f); canvas.drawLine(x, mRightDividerInsetTop, x, h - mRightDividerInsetBottom, mDividerPaint); } canvas.restore(); } private int getRealRadius(){ View owner = mOwner.get(); if (owner == null) { return mRadius; } int radius; if(mRadius == RADIUS_OF_HALF_VIEW_HEIGHT){ radius = owner.getHeight() /2; }else if(mRadius == RADIUS_OF_HALF_VIEW_WIDTH){ radius = owner.getWidth() / 2; }else{ radius = mRadius; } return radius; } public void dispatchRoundBorderDraw(Canvas canvas) { View owner = mOwner.get(); if (owner == null) { return; } int radius = getRealRadius(); boolean needCheckFakeOuterNormalDraw = radius > 0 && !useFeature() && mOuterNormalColor != 0; boolean needDrawBorder = mBorderWidth > 0 && mBorderColor != 0; if (!needCheckFakeOuterNormalDraw && !needDrawBorder) { return; } if (mIsShowBorderOnlyBeforeL && useFeature() && mShadowElevation != 0) { return; } int width = owner.getWidth(), height = owner.getHeight(); canvas.save(); canvas.translate(owner.getScrollX(), owner.getScrollY()); // react float halfBorderWith = mBorderWidth / 2f; if (mIsOutlineExcludePadding) { mBorderRect.set( owner.getPaddingLeft() + halfBorderWith, owner.getPaddingTop() + halfBorderWith, width - owner.getPaddingRight() - halfBorderWith, height - owner.getPaddingBottom() - halfBorderWith); } else { mBorderRect.set(halfBorderWith, halfBorderWith, width- halfBorderWith, height - halfBorderWith); } if(mShouldUseRadiusArray){ if(mRadiusArray == null){ mRadiusArray = new float[8]; } if (mHideRadiusSide == HIDE_RADIUS_SIDE_TOP) { mRadiusArray[4] = radius; mRadiusArray[5] = radius; mRadiusArray[6] = radius; mRadiusArray[7] = radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_RIGHT) { mRadiusArray[0] = radius; mRadiusArray[1] = radius; mRadiusArray[6] = radius; mRadiusArray[7] = radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_BOTTOM) { mRadiusArray[0] = radius; mRadiusArray[1] = radius; mRadiusArray[2] = radius; mRadiusArray[3] = radius; } else if (mHideRadiusSide == HIDE_RADIUS_SIDE_LEFT) { mRadiusArray[2] = radius; mRadiusArray[3] = radius; mRadiusArray[4] = radius; mRadiusArray[5] = radius; } } if (needCheckFakeOuterNormalDraw) { int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG); canvas.drawColor(mOuterNormalColor); mClipPaint.setColor(mOuterNormalColor); mClipPaint.setStyle(Paint.Style.FILL); mClipPaint.setXfermode(mMode); if (!mShouldUseRadiusArray) { canvas.drawRoundRect(mBorderRect, radius, radius, mClipPaint); } else { drawRoundRect(canvas, mBorderRect, mRadiusArray, mClipPaint); } mClipPaint.setXfermode(null); canvas.restoreToCount(layerId); } if (needDrawBorder) { mClipPaint.setColor(mBorderColor); mClipPaint.setStrokeWidth(mBorderWidth); mClipPaint.setStyle(Paint.Style.STROKE); if (mShouldUseRadiusArray) { drawRoundRect(canvas, mBorderRect, mRadiusArray, mClipPaint); } else if (radius <= 0) { canvas.drawRect(mBorderRect, mClipPaint); } else { canvas.drawRoundRect(mBorderRect, radius, radius, mClipPaint); } } canvas.restore(); } private void drawRoundRect(Canvas canvas, RectF rect, float[] radiusArray, Paint paint) { mPath.reset(); mPath.addRoundRect(rect, radiusArray, Path.Direction.CW); canvas.drawPath(mPath, paint); } public static boolean useFeature() { return Build.VERSION.SDK_INT >= 21; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUILinearLayout.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.layout; import android.content.Context; import android.graphics.Canvas; import androidx.annotation.ColorInt; import android.util.AttributeSet; import com.qmuiteam.qmui.alpha.QMUIAlphaLinearLayout; /** * @author cginechen * @date 2017-03-10 */ public class QMUILinearLayout extends QMUIAlphaLinearLayout implements IQMUILayout { private QMUILayoutHelper mLayoutHelper; public QMUILinearLayout(Context context) { super(context); init(context, null, 0); } public QMUILinearLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUILinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenPress(false); setChangeAlphaWhenDisable(false); } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIPriorityLinearLayout.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.layout; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import com.qmuiteam.qmui.R; import java.util.ArrayList; public class QMUIPriorityLinearLayout extends QMUILinearLayout { private ArrayList mTempMiniWidthChildList = new ArrayList<>(); private ArrayList mTempDisposableChildList = new ArrayList<>(); public QMUIPriorityLinearLayout(Context context) { super(context); } public QMUIPriorityLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int orientation = getOrientation(); if (orientation == HORIZONTAL) { handleHorizontal(widthMeasureSpec, heightMeasureSpec); } else { handleVertical(widthMeasureSpec, heightMeasureSpec); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private void handleHorizontal(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int visibleChildCount = getVisibleChildCount(); if (widthMode == MeasureSpec.UNSPECIFIED || visibleChildCount == 0 || widthSize <= 0) { return; } int usedWidth = handlePriorityIncompressible(widthMeasureSpec, heightMeasureSpec); if (usedWidth >= widthSize) { for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); view.measure(MeasureSpec.makeMeasureSpec(lp.miniContentProtectionSize, MeasureSpec.AT_MOST), heightMeasureSpec); lp.width = view.getMeasuredWidth(); } for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.width = 0; lp.leftMargin = 0; lp.rightMargin = 0; } } else { int usefulWidth = widthSize - usedWidth; int miniNeedWidth = 0, miniWidthChildTotalWidth = 0, marginHor; for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); view.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST), heightMeasureSpec); marginHor = lp.leftMargin + lp.rightMargin; miniWidthChildTotalWidth += view.getMeasuredWidth() + marginHor; miniNeedWidth += Math.min(view.getMeasuredWidth(), lp.miniContentProtectionSize) + marginHor; } if (miniNeedWidth >= usefulWidth) { for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.width = Math.min(view.getMeasuredWidth(), lp.miniContentProtectionSize); } for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.width = 0; lp.leftMargin = 0; lp.rightMargin = 0; } } else if (miniWidthChildTotalWidth < usefulWidth) { // there is a space for disposableChildList if (!mTempDisposableChildList.isEmpty()) { dispatchSpaceToDisposableChildList(mTempDisposableChildList, widthMeasureSpec, heightMeasureSpec, usefulWidth - miniWidthChildTotalWidth); } } else { // no space for disposableChild for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.width = 0; lp.leftMargin = 0; lp.rightMargin = 0; } if (usefulWidth < miniWidthChildTotalWidth && !mTempMiniWidthChildList.isEmpty()) { dispatchSpaceToMiniWidthChildList(mTempMiniWidthChildList, usefulWidth, miniWidthChildTotalWidth); } } } } private void handleVertical(int widthMeasureSpec, int heightMeasureSpec) { int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int visibleChildCount = getVisibleChildCount(); if (heightMode == MeasureSpec.UNSPECIFIED || visibleChildCount == 0 || heightSize <= 0) { return; } int usedHeight = handlePriorityIncompressible(widthMeasureSpec, heightMeasureSpec); if (usedHeight >= heightSize) { for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); view.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(lp.miniContentProtectionSize, MeasureSpec.AT_MOST)); lp.height = view.getMeasuredHeight(); } for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.height = 0; lp.topMargin = 0; lp.bottomMargin = 0; } } else { int usefulSpace = heightSize - usedHeight; int miniNeedSpace = 0, miniSizeChildTotalLength = 0, marginVer; for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); view.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)); marginVer = lp.topMargin + lp.bottomMargin; miniSizeChildTotalLength += view.getMeasuredHeight() + marginVer; miniNeedSpace += Math.min(view.getMeasuredHeight(), lp.miniContentProtectionSize) + marginVer; } if (miniNeedSpace >= usefulSpace) { for (View view : mTempMiniWidthChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.height = Math.min(view.getMeasuredHeight(), lp.miniContentProtectionSize); } for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.height = 0; lp.topMargin = 0; lp.bottomMargin = 0; } } else if (miniSizeChildTotalLength < usefulSpace) { // there is a space for disposableChildList if (!mTempDisposableChildList.isEmpty()) { dispatchSpaceToDisposableChildList(mTempDisposableChildList, widthMeasureSpec, heightMeasureSpec, usefulSpace - miniSizeChildTotalLength); } } else { // no space for disposableChild for (View view : mTempDisposableChildList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.height = 0; lp.topMargin = 0; lp.bottomMargin = 0; } if (usefulSpace < miniSizeChildTotalLength && !mTempMiniWidthChildList.isEmpty()) { dispatchSpaceToMiniWidthChildList(mTempMiniWidthChildList, usefulSpace, miniSizeChildTotalLength); } } } } private int handlePriorityIncompressible(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int usedSize = 0; mTempMiniWidthChildList.clear(); mTempDisposableChildList.clear(); int orientation = getOrientation(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.backupOrRestore(); int priority = lp.getPriority(orientation); int margin = orientation == HORIZONTAL ? lp.leftMargin + lp.rightMargin : lp.topMargin + lp.bottomMargin; if (priority == LayoutParams.PRIORITY_INCOMPRESSIBLE) { if (orientation == HORIZONTAL) { if (lp.width >= 0) { usedSize += lp.width + margin; } else { child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST), heightMeasureSpec); usedSize += child.getMeasuredWidth() + margin; } } else { if (lp.height >= 0) { usedSize += lp.height + margin; } else { child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)); usedSize += child.getMeasuredHeight() + margin; } } } else if (priority == LayoutParams.PRIORITY_MINI_CONTENT_PROTECTION) { mTempMiniWidthChildList.add(child); } else { if (lp.weight == 0) { mTempDisposableChildList.add(child); } } } return usedSize; } protected void dispatchSpaceToDisposableChildList(ArrayList childList, int widthMeasureSpec, int heightMeasureSpec, int usefulSpace) { for (View view : childList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (getOrientation() == HORIZONTAL) { if(usefulSpace <= 0){ lp.leftMargin = 0; lp.rightMargin = 0; lp.width = 0; } usefulSpace -= lp.leftMargin - lp.rightMargin; if(usefulSpace > 0){ view.measure( MeasureSpec.makeMeasureSpec(usefulSpace, MeasureSpec.AT_MOST), getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height)); if(view.getMeasuredWidth() >= usefulSpace){ lp.width = usefulSpace; usefulSpace = 0; }else{ usefulSpace -= view.getMeasuredWidth(); } }else{ lp.leftMargin = 0; lp.rightMargin = 0; lp.width = 0; } } else { if(usefulSpace <= 0){ lp.topMargin = 0; lp.bottomMargin = 0; lp.height = 0; } usefulSpace -= lp.topMargin - lp.bottomMargin; if(usefulSpace > 0){ view.measure( getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width), MeasureSpec.makeMeasureSpec(usefulSpace, MeasureSpec.AT_MOST)); if(view.getMeasuredHeight() >= usefulSpace){ lp.height = usefulSpace; usefulSpace = 0; }else{ usefulSpace -= view.getMeasuredHeight(); } }else{ lp.topMargin = 0; lp.bottomMargin = 0; lp.height = 0; } } } } protected void dispatchSpaceToMiniWidthChildList(ArrayList childList, int usefulSpace, int calculateTotalLength) { int extra = calculateTotalLength - usefulSpace; if (extra > 0) { for (View view : childList) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (getOrientation() == HORIZONTAL) { float radio = (view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin) * 1f / calculateTotalLength; int width = (int) (view.getMeasuredWidth() - extra * radio); lp.width = Math.max(0, width); } else { float radio = (view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin) * 1f / calculateTotalLength; int height = (int) (view.getMeasuredHeight() - extra * radio); lp.height = Math.max(0, height); } } } } private int getVisibleChildCount() { int childCount = getChildCount(); int visibleChildCount = 0; for (int i = 0; i < childCount; i++) { if (getChildAt(i).getVisibility() == VISIBLE) { visibleChildCount++; } } return visibleChildCount; } @Override protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { return new LayoutParams(lp); } @Override public LinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } public static class LayoutParams extends LinearLayout.LayoutParams { public static final int PRIORITY_DISPOSABLE = 1; public static final int PRIORITY_MINI_CONTENT_PROTECTION = 2; public static final int PRIORITY_INCOMPRESSIBLE = 3; private int priority = PRIORITY_MINI_CONTENT_PROTECTION; private int miniContentProtectionSize = 0; private int backupWidth = Integer.MIN_VALUE; private int backupHeight = Integer.MIN_VALUE; private int backupLeftMargin = 0; private int backupRightMargin = 0; private int backupTopMargin = 0; private int backupBottomMargin = 0; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.QMUIPriorityLinearLayout_Layout); priority = a.getInteger(R.styleable.QMUIPriorityLinearLayout_Layout_qmui_layout_priority, PRIORITY_MINI_CONTENT_PROTECTION); miniContentProtectionSize = a.getDimensionPixelSize( R.styleable.QMUIPriorityLinearLayout_Layout_qmui_layout_miniContentProtectionSize, 0); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, float weight) { super(width, height, weight); } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } @TargetApi(19) public LayoutParams(LinearLayout.LayoutParams source) { super(source); } public void setPriority(int priority) { this.priority = priority; } public void setMiniContentProtectionSize(int miniContentProtectionSize) { this.miniContentProtectionSize = miniContentProtectionSize; } public int getPriority(int orientation) { if (weight > 0) { return PRIORITY_DISPOSABLE; } if (orientation == LinearLayout.HORIZONTAL) { if (width >= 0) { return PRIORITY_INCOMPRESSIBLE; } } else { if (height >= 0) { return PRIORITY_INCOMPRESSIBLE; } } return priority; } void backupOrRestore() { if (backupWidth == Integer.MIN_VALUE) { backupWidth = width; backupLeftMargin = leftMargin; backupRightMargin = rightMargin; } else { width = backupWidth; leftMargin = backupLeftMargin; rightMargin = backupRightMargin; } if (backupHeight == Integer.MIN_VALUE) { backupHeight = height; backupTopMargin = topMargin; backupBottomMargin = bottomMargin; } else { height = backupHeight; topMargin = backupTopMargin; bottomMargin = backupBottomMargin; } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/layout/QMUIRelativeLayout.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.layout; import android.content.Context; import android.graphics.Canvas; import androidx.annotation.ColorInt; import android.util.AttributeSet; import com.qmuiteam.qmui.alpha.QMUIAlphaRelativeLayout; /** * @author cginechen * @date 2017-03-10 */ public class QMUIRelativeLayout extends QMUIAlphaRelativeLayout implements IQMUILayout { private QMUILayoutHelper mLayoutHelper; public QMUIRelativeLayout(Context context) { super(context); init(context, null, 0); } public QMUIRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenDisable(false); setChangeAlphaWhenPress(false); } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/link/ITouchableSpan.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.link; import android.view.View; /** * @author cginechen * @date 2017-03-20 */ public interface ITouchableSpan { void setPressed(boolean pressed); void onClick(View widget); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchDecorHelper.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.link; import android.text.Layout; import android.text.Selection; import android.text.Spannable; import android.util.Log; import android.view.MotionEvent; import android.widget.TextView; import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.widget.textview.ISpanTouchFix; import java.lang.ref.WeakReference; /** * @author cginechen * @date 2017-03-20 */ public class QMUILinkTouchDecorHelper { private WeakReference mPressedSpanRf; public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { ITouchableSpan span = getPressedSpan(textView, spannable, event); if (span != null) { span.setPressed(true); Selection.setSelection(spannable, spannable.getSpanStart(span), spannable.getSpanEnd(span)); mPressedSpanRf = new WeakReference<>(span); } if (textView instanceof ISpanTouchFix) { ISpanTouchFix tv = (ISpanTouchFix) textView; tv.setTouchSpanHit(span != null); } return span != null; } else if (event.getAction() == MotionEvent.ACTION_MOVE) { ITouchableSpan touchedSpan = getPressedSpan(textView, spannable, event); ITouchableSpan recordSpan = null; if (mPressedSpanRf != null){ recordSpan = mPressedSpanRf.get(); } if(recordSpan != null && recordSpan != touchedSpan){ recordSpan.setPressed(false); mPressedSpanRf = null; recordSpan = null; Selection.removeSelection(spannable); } if (textView instanceof ISpanTouchFix) { ISpanTouchFix tv = (ISpanTouchFix) textView; tv.setTouchSpanHit(recordSpan != null); } return recordSpan != null; } else if (event.getAction() == MotionEvent.ACTION_UP) { boolean touchSpanHint = false; ITouchableSpan recordSpan = null; if (mPressedSpanRf != null){ recordSpan = mPressedSpanRf.get(); } if (recordSpan != null) { touchSpanHint = true; recordSpan.setPressed(false); if(event.getAction() == MotionEvent.ACTION_UP){ recordSpan.onClick(textView); } } mPressedSpanRf = null; Selection.removeSelection(spannable); if (textView instanceof ISpanTouchFix) { ISpanTouchFix tv = (ISpanTouchFix) textView; tv.setTouchSpanHit(touchSpanHint); } return touchSpanHint; } else { ITouchableSpan recordSpan = null; if (mPressedSpanRf != null){ recordSpan = mPressedSpanRf.get(); } if (recordSpan != null) { recordSpan.setPressed(false); } if (textView instanceof ISpanTouchFix) { ISpanTouchFix tv = (ISpanTouchFix) textView; tv.setTouchSpanHit(false); } mPressedSpanRf = null; Selection.removeSelection(spannable); return false; } } public ITouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); x -= textView.getTotalPaddingLeft(); y -= textView.getTotalPaddingTop(); x += textView.getScrollX(); y += textView.getScrollY(); Layout layout = textView.getLayout(); int line = layout.getLineForVertical(y); /* * BugFix: https://issuetracker.google.com/issues/113348914 */ try { int off = layout.getOffsetForHorizontal(line, x); if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) { // 实际上没点到任何内容 off = -1; } ITouchableSpan[] link = spannable.getSpans(off, off, ITouchableSpan.class); ITouchableSpan touchedSpan = null; if (link.length > 0) { touchedSpan = link[0]; } return touchedSpan; } catch (IndexOutOfBoundsException e) { if (QMUIConfig.DEBUG) { Log.d(this.toString(), "getPressedSpan", e); } } return null; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkTouchMovementMethod.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.link; import android.text.Spannable; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.text.method.Touch; import android.view.MotionEvent; import android.widget.TextView; /** * 配合 {@link QMUILinkTouchDecorHelper} 使用 * * @author cginechen * @date 2017-03-20 */ public class QMUILinkTouchMovementMethod extends LinkMovementMethod { @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { return sHelper.onTouchEvent(widget, buffer, event) || Touch.onTouchEvent(widget, buffer, event); } public static MovementMethod getInstance() { if (sInstance == null) sInstance = new QMUILinkTouchMovementMethod(); return sInstance; } private static QMUILinkTouchMovementMethod sInstance; private static QMUILinkTouchDecorHelper sHelper = new QMUILinkTouchDecorHelper(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/link/QMUILinkify.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.link; /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.content.res.ColorStateList; import android.graphics.Color; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.text.style.URLSpan; import android.util.Patterns; import android.view.View; import android.webkit.WebView; import android.widget.TextView; import com.qmuiteam.qmui.span.QMUIOnSpanClickListener; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Linkify take a piece of text and a regular expression and turns all of the * regex matches in the text into clickable links. This is particularly * useful for matching things like email addresses, web urls, etc. and making * them actionable. *

* Alone with the pattern that is to be matched, a url scheme prefix is also * required. Any pattern match that does not begin with the supplied scheme * will have the scheme prepended to the matched text when the clickable url * is created. For instance, if you are matching web urls you would supply * the scheme http://. If the pattern matches example.com, which * does not have a url scheme prefix, the supplied scheme will be prepended to * create http://example.com when the clickable url link is * created. */ public class QMUILinkify { public static final Pattern WECHAT_PHONE = Pattern.compile("\\+?(\\d{2,8}([- ]?\\d{3,8}){2,6}|\\d{5,20})"); // 其他数字的情况 public static final Pattern NOT_PHONE = Pattern.compile("^\\d+(\\.\\d+)+(-\\d+)*$"); private static final String UrlEndAppendNextChars = "[$]"; /** * Bit field indicating that web URLs should be matched in methods that * take an options mask */ public static final int WEB_URLS = 0x01; /** * Bit field indicating that email addresses should be matched in methods * that take an options mask */ public static final int EMAIL_ADDRESSES = 0x02; /** * Bit field indicating that phone numbers should be matched in methods that * take an options mask */ public static final int PHONE_NUMBERS = 0x04; /** * Bit field indicating that street addresses should be matched in methods that * take an options mask */ public static final int MAP_ADDRESSES = 0x08; /** * Bit mask indicating that all available patterns should be matched in * methods that take an options mask */ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES; /** * Don't treat anything with fewer than this many digits as a * phone number. */ private static final int PHONE_NUMBER_MINIMUM_DIGITS = 7; public static final WebUrlMatcher QMUI_WEB_URL_MATCHER = new WebUrlMatcher() { @Override public Pattern getPattern() { return WebUrlPattern.WEB_URL; } }; private static WebUrlMatcher sWebUrlMatcher = new WebUrlMatcher() { @Override public Pattern getPattern() { return Patterns.WEB_URL; } }; public static void useQmuiWebUrlMatcher(){ sWebUrlMatcher = QMUI_WEB_URL_MATCHER; } public static void setWebUrlMatcher(WebUrlMatcher webUrlMatcher) { sWebUrlMatcher = webUrlMatcher; } /** * Filters out web URL matches that occur after an at-sign (@). This is * to prevent turning the domain name in an email address into a web link. */ public static final MatchFilter sUrlMatchFilter = new MatchFilter() { public final boolean acceptMatch(CharSequence s, int start, int end) { try { for (int i = start; i < end; ++i) { if (s.charAt(i) > 256) { return false; } } try { char nextChar = s.charAt(end); if (nextChar < 256 && !((0 <= UrlEndAppendNextChars.indexOf(nextChar)) || Character.isWhitespace(nextChar))) { return false; } } catch (Exception ignored) { } if (start == 0) { return true; } if (s.charAt(start - 1) == '@') { return false; } } catch (Exception ignored) { } return true; } }; /** * Filters out URL matches that don't have enough digits to be a * phone number. */ public static final MatchFilter sPhoneNumberMatchFilter = new MatchFilter() { public final boolean acceptMatch(CharSequence s, int start, int end) { int digitCount = 0; for (int i = start; i < end; i++) { if (Character.isDigit(s.charAt(i))) { digitCount++; if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) { return true; } } } return false; } }; /** * Transforms matched phone number text into something suitable * to be used in a tel: URL. It does this by removing everything * but the digits and plus signs. For instance: * '+1 (919) 555-1212' * becomes '+19195551212' */ public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() { public final String transformUrl(final Matcher match, String url) { return Patterns.digitsAndPlusOnly(match); } }; /** * MatchFilter enables client code to have more control over * what is allowed to match and become a link, and what is not. *

* For example: when matching web urls you would like things like * http://www.example.com to match, as well as just example.com itelf. * However, you would not want to match against the domain in * support@example.com. So, when matching against a web url pattern you * might also include a MatchFilter that disallows the match if it is * immediately preceded by an at-sign (@). */ public interface MatchFilter { /** * Examines the character span matched by the pattern and determines * if the match should be turned into an actionable link. * * @param s The body of text against which the pattern * was matched * @param start The index of the first character in s that was * matched by the pattern - inclusive * @param end The index of the last character in s that was * matched - exclusive * @return Whether this match should be turned into a link */ boolean acceptMatch(CharSequence s, int start, int end); } /** * TransformFilter enables client code to have more control over * how matched patterns are represented as URLs. *

* For example: when converting a phone number such as (919) 555-1212 * into a tel: URL the parentheses, white space, and hyphen need to be * removed to produce tel:9195551212. */ public interface TransformFilter { /** * Examines the matched text and either passes it through or uses the * data in the Matcher state to produce a replacement. * * @param match The regex matcher state that found this URL text * @param url The text that was matched * @return The transformed form of the URL */ String transformUrl(final Matcher match, String url); } /** * Scans the text of the provided Spannable and turns all occurrences * of the link types indicated in the mask into clickable links. * If the mask is nonzero, it also removes any existing URLSpans * attached to the Spannable, to avoid problems if you call it * repeatedly on the same text. */ public static boolean addLinks(Spannable text, int mask, ColorStateList linkColor, ColorStateList bgColor, QMUIOnSpanClickListener l) { if (mask == 0) { return false; } URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); for (int i = old.length - 1; i >= 0; i--) { text.removeSpan(old[i]); } ArrayList links = new ArrayList<>(); if ((mask & WEB_URLS) != 0) { gatherLinks(links, text, sWebUrlMatcher.getPattern(), new String[]{"http://", "https://", "rtsp://"}, sUrlMatchFilter, null); } if ((mask & EMAIL_ADDRESSES) != 0) { gatherLinks(links, text, Patterns.EMAIL_ADDRESS, new String[]{"mailto:"}, null, null); } if ((mask & PHONE_NUMBERS) != 0) { gatherPhoneLinks(links, text, WECHAT_PHONE, new Pattern[]{NOT_PHONE}, new String[]{"tel:"}, sPhoneNumberMatchFilter, sPhoneNumberTransformFilter); } if ((mask & MAP_ADDRESSES) != 0) { gatherMapLinks(links, text); } pruneOverlaps(links); if (links.size() == 0) { return false; } for (LinkSpec link : links) { applyLink(link.url, link.start, link.end, text, linkColor, bgColor, l); } return true; } /** * Scans the text of the provided TextView and turns all occurrences of * the link types indicated in the mask into clickable links. If matches * are found the movement method for the TextView is set to * LinkMovementMethod. */ public static boolean addLinks(TextView text, int mask, ColorStateList linkColor, ColorStateList bgColor, QMUIOnSpanClickListener l) { if (mask == 0) { return false; } CharSequence t = text.getText(); if (t instanceof Spannable) { if (addLinks((Spannable) t, mask, linkColor, bgColor, l)) { addLinkMovementMethod(text); return true; } return false; } else { SpannableString s = SpannableString.valueOf(t); if (addLinks(s, mask, linkColor, bgColor, l)) { addLinkMovementMethod(text); text.setText(s); return true; } return false; } } private static void addLinkMovementMethod(TextView t) { MovementMethod m = t.getMovementMethod(); if ((m == null) || !(m instanceof LinkMovementMethod)) { if (t.getLinksClickable()) { t.setMovementMethod(LinkMovementMethod.getInstance()); } } } /** * Applies a regex to the text of a TextView turning the matches into * links. If links are found then UrlSpans are applied to the link * text match areas, and the movement method for the text is changed * to LinkMovementMethod. * * @param text TextView whose text is to be marked-up with links * @param pattern Regex pattern to be used for finding links * @param scheme Url scheme string (eg http:// to be * prepended to the url of links that do not have * a scheme specified in the link text */ public static void addLinks(TextView text, Pattern pattern, String scheme) { addLinks(text, pattern, scheme, null, null); } /** * Applies a regex to the text of a TextView turning the matches into * links. If links are found then UrlSpans are applied to the link * text match areas, and the movement method for the text is changed * to LinkMovementMethod. * * @param text TextView whose text is to be marked-up with links * @param p Regex pattern to be used for finding links * @param scheme Url scheme string (eg http:// to be * prepended to the url of links that do not have * a scheme specified in the link text * @param matchFilter The filter that is used to allow the client code * additional control over which pattern matches are * to be converted into links. */ public static void addLinks(TextView text, Pattern p, String scheme, MatchFilter matchFilter, TransformFilter transformFilter) { SpannableString s = SpannableString.valueOf(text.getText()); if (addLinks(s, p, scheme, matchFilter, transformFilter)) { text.setText(s); addLinkMovementMethod(text); } } /** * Applies a regex to a Spannable turning the matches into * links. * * @param text Spannable whose text is to be marked-up with * links * @param pattern Regex pattern to be used for finding links * @param scheme Url scheme string (eg http:// to be * prepended to the url of links that do not have * a scheme specified in the link text */ public static boolean addLinks(Spannable text, Pattern pattern, String scheme) { return addLinks(text, pattern, scheme, null, null); } /** * Applies a regex to a Spannable turning the matches into * links. * * @param s Spannable whose text is to be marked-up with * links * @param p Regex pattern to be used for finding links * @param scheme Url scheme string (eg http:// to be * prepended to the url of links that do not have * a scheme specified in the link text * @param matchFilter The filter that is used to allow the client code * additional control over which pattern matches are * to be converted into links. */ public static boolean addLinks(Spannable s, Pattern p, String scheme, MatchFilter matchFilter, TransformFilter transformFilter) { boolean hasMatches = false; String prefix = (scheme == null) ? "" : scheme.toLowerCase(Locale.ROOT); Matcher m = p.matcher(s); while (m.find()) { int start = m.start(); int end = m.end(); boolean allowed = true; if (matchFilter != null) { allowed = matchFilter.acceptMatch(s, start, end); } if (allowed) { String url = makeUrl(m.group(0), new String[]{prefix}, m, transformFilter); applyLink(url, start, end, s, null, null, null); hasMatches = true; } } return hasMatches; } private static void applyLink(String url, int start, int end, Spannable text, final ColorStateList linkColor, final ColorStateList bgColor, QMUIOnSpanClickListener l) { text.setSpan(new StyleableURLSpan(url, l) { @Override public void updateDrawState(TextPaint ds) { if (linkColor != null) { int normalLinkColor = linkColor.getColorForState(new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed}, Color.TRANSPARENT); int pressedLinkColor = linkColor.getColorForState(new int[]{android.R.attr.state_pressed}, normalLinkColor); ds.linkColor = mPressed ? pressedLinkColor : normalLinkColor; } if (bgColor != null) { int normalBgColor = bgColor.getColorForState(new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed}, Color.TRANSPARENT); int pressedBgColor = bgColor.getColorForState(new int[]{android.R.attr.state_pressed}, normalBgColor); ds.bgColor = mPressed ? pressedBgColor : normalBgColor; } super.updateDrawState(ds); ds.setUnderlineText(false); } }, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } private static abstract class StyleableURLSpan extends URLSpan implements ITouchableSpan { protected boolean mPressed = false; protected String mUrl; protected QMUIOnSpanClickListener mOnSpanClickListener; public StyleableURLSpan(String url, QMUIOnSpanClickListener l) { super(url); mUrl = url; mOnSpanClickListener = l; } @Override public void setPressed(boolean pressed) { mPressed = pressed; } @Override public void onClick(View widget) { if (mOnSpanClickListener.onSpanClick(mUrl)) { return; } super.onClick(widget); } } private static String makeUrl(String url, String[] prefixes, Matcher m, TransformFilter filter) { if (filter != null) { url = filter.transformUrl(m, url); } boolean hasPrefix = false; for (String prefixe : prefixes) { if (url.regionMatches(true, 0, prefixe, 0, prefixe.length())) { hasPrefix = true; // Fix capitalization if necessary if (!url.regionMatches(false, 0, prefixe, 0, prefixe.length())) { url = prefixe + url.substring(prefixe.length()); } break; } } if (!hasPrefix) { url = prefixes[0] + url; } return url; } private static void gatherLinks(ArrayList links, Spannable s, Pattern pattern, String[] schemes, MatchFilter matchFilter, TransformFilter transformFilter) { Matcher m = pattern.matcher(s); while (m.find()) { int start = m.start(); int end = m.end(); if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) { LinkSpec spec = new LinkSpec(); spec.url = makeUrl(m.group(0), schemes, m, transformFilter); spec.start = start; spec.end = end; links.add(spec); } } } private static void gatherPhoneLinks(ArrayList links, Spannable s, Pattern pattern, Pattern[] excepts, String[] schemes, MatchFilter matchFilter, TransformFilter transformFilter) { Matcher m = pattern.matcher(s); while (m.find()) { if (isInExcepts(m.group(), excepts)) { continue; } int start = m.start(); int end = m.end(); if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) { LinkSpec spec = new LinkSpec(); spec.url = makeUrl(m.group(0), schemes, m, transformFilter); spec.start = start; spec.end = end; links.add(spec); } } } private static boolean isInExcepts(CharSequence data, Pattern[] excepts) { for (Pattern except : excepts) { Matcher m = except.matcher(data); if (m.find()) { return true; } } return isTooLarge(data); } private final static int MAX_NUMBER = 21; private static boolean isTooLarge(CharSequence data) { if (data.length() <= MAX_NUMBER) { return false; } final int count = data.length(); int digitCount = 0; for (int i = 0; i < count; i++) { if (Character.isDigit(data.charAt(i))) { digitCount++; if (digitCount > MAX_NUMBER) { return true; } } } return false; } // private static final void gatherTelLinks(ArrayList links, Spannable s) { // PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); // Iterable matches = phoneUtil.findNumbers(s.toString(), // Locale.getDefault().getCountry(), Leniency.POSSIBLE, Long.MAX_VALUE); // for (PhoneNumberMatch match : matches) { // LinkSpec spec = new LinkSpec(); // spec.url = "tel:" + PhoneNumberUtils.normalizeNumber(match.rawString()); // spec.start = match.start(); // spec.end = match.end(); // links.add(spec); // } // } private static void gatherMapLinks(ArrayList links, Spannable s) { String string = s.toString(); String address; int base = 0; try { while ((address = WebView.findAddress(string)) != null) { int start = string.indexOf(address); if (start < 0) { break; } LinkSpec spec = new LinkSpec(); int length = address.length(); int end = start + length; spec.start = base + start; spec.end = base + end; string = string.substring(end); base += end; String encodedAddress; try { encodedAddress = URLEncoder.encode(address, "UTF-8"); } catch (UnsupportedEncodingException e) { continue; } spec.url = "geo:0,0?q=" + encodedAddress; links.add(spec); } } catch (UnsupportedOperationException e) { // findAddress may fail with an unsupported exception on platforms without a WebView. // In this case, we will not append anything to the links variable: it would have died // in WebView.findAddress. } } private static void pruneOverlaps(ArrayList links) { Comparator c = new Comparator() { public final int compare(LinkSpec a, LinkSpec b) { if (a.start < b.start) { return -1; } if (a.start > b.start) { return 1; } if (a.end < b.end) { return 1; } if (a.end > b.end) { return -1; } return 0; } }; Collections.sort(links, c); int len = links.size(); int i = 0; while (i < len - 1) { LinkSpec a = links.get(i); LinkSpec b = links.get(i + 1); int remove = -1; if ((a.start <= b.start) && (a.end > b.start)) { if (b.end <= a.end) { remove = i + 1; } else if ((a.end - a.start) > (b.end - b.start)) { remove = i + 1; } else if ((a.end - a.start) < (b.end - b.start)) { remove = i; } if (remove != -1) { links.remove(remove); len--; continue; } } i++; } } private static class LinkSpec { String url; int start; int end; } private static class WebUrlPattern { // all domain names private static final String[] EXT = { "top", "com", "net", "org", "edu", "gov", "int", "mil", "tel", "biz", "cc", "tv", "info", "zw", "name", "hk", "mobi", "asia", "cd", "travel", "pro", "museum", "coop", "aero", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cq", "cr", "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "es", "et", "ev", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gh", "gi", "gl", "gm", "gn", "gp", "gr", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "in", "io", "iq", "ir", "is", "it", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nt", "nu", "nz", "om", "qa", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "pt", "pw", "py", "re", "ro", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "su", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "va", "vc", "ve", "vg", "vn", "vu", "wf", "ws", "ye", "yu", "za", "zm", "zr" }; private static final String PROTOCOL = "(?i:http|https|rtsp)://"; private static final String IP_ADDRESS = "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9]))"; /** * Valid UCS characters defined in RFC 3987. Excludes space characters. */ private static final String UCS_CHAR = "[" + "\u00A0-\uD7FF" + "\uF900-\uFDCF" + "\uFDF0-\uFFEF" + "\uD800\uDC00-\uD83F\uDFFD" + "\uD840\uDC00-\uD87F\uDFFD" + "\uD880\uDC00-\uD8BF\uDFFD" + "\uD8C0\uDC00-\uD8FF\uDFFD" + "\uD900\uDC00-\uD93F\uDFFD" + "\uD940\uDC00-\uD97F\uDFFD" + "\uD980\uDC00-\uD9BF\uDFFD" + "\uD9C0\uDC00-\uD9FF\uDFFD" + "\uDA00\uDC00-\uDA3F\uDFFD" + "\uDA40\uDC00-\uDA7F\uDFFD" + "\uDA80\uDC00-\uDABF\uDFFD" + "\uDAC0\uDC00-\uDAFF\uDFFD" + "\uDB00\uDC00-\uDB3F\uDFFD" + "\uDB44\uDC00-\uDB7F\uDFFD" + "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]"; /** * Valid characters for IRI label defined in RFC 3987. */ private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR; private static final String PORT_NUMBER = "\\:\\d{1,5}"; private static final String PATH_AND_QUERY = "[/\\?](?:(?:[" + LABEL_CHAR + ";/\\?:@&=#~" // plus optional query params + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"; private static Pattern WEB_URL; static { StringBuilder sb = new StringBuilder(); sb.append("("); for (int i = 0; i < EXT.length; i++) { if(i != 0){ sb.append("|"); } sb.append(EXT[i]); } sb.append(")"); String host = "((?:(www\\.|[a-zA-Z\\.\\-]+\\.)?[a-zA-Z0-9\\-]+)" + "\\." + sb.toString() + ")"; WEB_URL = Pattern.compile("(" + "(" + PROTOCOL + ")?" + "(" + IP_ADDRESS + "|" + host +")" + "(" + PORT_NUMBER + ")?" + "(" + PATH_AND_QUERY + ")?" + ")"); } } public interface WebUrlMatcher { Pattern getPattern(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/link/QMUIScrollingMovementMethod.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.link; import android.text.Spannable; import android.text.method.MovementMethod; import android.text.method.ScrollingMovementMethod; import android.text.method.Touch; import android.view.MotionEvent; import android.widget.TextView; /** * @author cginechen * @date 2017-03-20 */ public class QMUIScrollingMovementMethod extends ScrollingMovementMethod { @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { return sHelper.onTouchEvent(widget, buffer, event) || Touch.onTouchEvent(widget, buffer, event); } public static MovementMethod getInstance() { if (sInstance == null) sInstance = new QMUIScrollingMovementMethod(); return sInstance; } private static QMUIScrollingMovementMethod sInstance; private static QMUILinkTouchDecorHelper sHelper = new QMUILinkTouchDecorHelper(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/IQMUIContinuousNestedBottomView.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.nestedScroll; public interface IQMUIContinuousNestedBottomView extends IQMUIContinuousNestedScrollCommon { int HEIGHT_IS_ENOUGH_TO_SCROLL = -1; /** * consume scroll * * @param dyUnconsumed the delta value to consume */ void consumeScroll(int dyUnconsumed); void smoothScrollYBy(int dy, int duration); void stopScroll(); /** * sometimes the content of BottomView is not enough to scroll, * so BottomView should tell the this info to {@link QMUIContinuousNestedScrollLayout} * * @return {@link #HEIGHT_IS_ENOUGH_TO_SCROLL} if can scroll, or content height. */ int getContentHeight(); int getCurrentScroll(); int getScrollOffsetRange(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/IQMUIContinuousNestedScrollCommon.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.nestedScroll; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public interface IQMUIContinuousNestedScrollCommon { int SCROLL_STATE_IDLE = RecyclerView.SCROLL_STATE_IDLE; int SCROLL_STATE_DRAGGING = RecyclerView.SCROLL_STATE_DRAGGING; int SCROLL_STATE_SETTLING = RecyclerView.SCROLL_STATE_SETTLING; void saveScrollInfo(@NonNull Bundle bundle); void restoreScrollInfo(@NonNull Bundle bundle); void injectScrollNotifier(OnScrollNotifier notifier); interface OnScrollNotifier { void notify(int innerOffset, int innerRange); void onScrollStateChange(View view, int newScrollState); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/IQMUIContinuousNestedTopView.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.nestedScroll; public interface IQMUIContinuousNestedTopView extends IQMUIContinuousNestedScrollCommon { /** * consume scroll * * @param dyUnconsumed the delta value to consume * @return the remain unconsumed value */ int consumeScroll(int dyUnconsumed); int getCurrentScroll(); int getScrollOffsetRange(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomAreaBehavior.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.nestedScroll; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import java.util.List; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.GravityCompat; public class QMUIContinuousNestedBottomAreaBehavior extends QMUIViewOffsetBehavior { private final Rect tempRect1 = new Rect(); private final Rect tempRect2 = new Rect(); private int mTopInset = 0; public void setTopInset(int topInset) { mTopInset = topInset; } public QMUIContinuousNestedBottomAreaBehavior() { } public QMUIContinuousNestedBottomAreaBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final int childLpHeight = child.getLayoutParams().height; if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec); if (availableHeight == 0) { availableHeight = parent.getHeight(); } availableHeight -= mTopInset; final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( availableHeight, childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST); parent.onMeasureChild( child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); return true; } return false; } @Override protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) { List dependencies = parent.getDependencies(child); if (!dependencies.isEmpty()) { View topView = dependencies.get(0); final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); final Rect available = tempRect1; available.set( parent.getPaddingLeft() + lp.leftMargin, topView.getBottom() + lp.topMargin, parent.getWidth() - parent.getPaddingRight() - lp.rightMargin, parent.getHeight() + topView.getBottom() - parent.getPaddingBottom() - lp.bottomMargin); final Rect out = tempRect2; GravityCompat.apply( resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); } else { super.layoutChild(parent, child, layoutDirection); } } @Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { boolean ret = super.onLayoutChild(parent, child, layoutDirection); List dependencies = parent.getDependencies(child); if (!dependencies.isEmpty()) { View topView = dependencies.get(0); setTopAndBottomOffset(topView.getBottom() - getLayoutTop()); } return ret; } private static int resolveGravity(int gravity) { return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity; } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { return dependency instanceof IQMUIContinuousNestedTopView; } @Override public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { setTopAndBottomOffset(dependency.getBottom() - getLayoutTop()); return false; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomDelegateLayout.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.nestedScroll; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.OverScroller; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import androidx.annotation.NonNull; import androidx.core.view.NestedScrollingChild; import androidx.core.view.NestedScrollingChild2; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.NestedScrollingParent2; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ViewCompat; import static com.qmuiteam.qmui.QMUIInterpolatorStaticHolder.QUNITIC_INTERPOLATOR; public abstract class QMUIContinuousNestedBottomDelegateLayout extends QMUIFrameLayout implements NestedScrollingChild2, NestedScrollingParent2, IQMUIContinuousNestedBottomView { public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_scroll_info_bottom_dl_offset"; private final NestedScrollingParentHelper mParentHelper; private final NestedScrollingChildHelper mChildHelper; private View mHeaderView; private View mContentView; private QMUIViewOffsetHelper mHeaderViewOffsetHelper; private QMUIViewOffsetHelper mContentViewOffsetHelper; private IQMUIContinuousNestedBottomView.OnScrollNotifier mOnScrollNotifier; private static final int INVALID_POINTER = -1; private boolean isBeingDragged; private int activePointerId = INVALID_POINTER; private int lastMotionY; private int touchSlop = -1; private VelocityTracker velocityTracker; private final ViewFlinger mViewFlinger; private final int[] mScrollConsumed = new int[2]; private final int[] mScrollOffset = new int[2]; private Rect mTempRect = new Rect(); private int mNestedOffsetY = 0; private Runnable mCheckLayoutAction = new Runnable() { @Override public void run() { checkLayout(); } }; public QMUIContinuousNestedBottomDelegateLayout(Context context) { this(context, null); } public QMUIContinuousNestedBottomDelegateLayout(Context context, AttributeSet attrs) { this(context, null, 0); } public QMUIContinuousNestedBottomDelegateLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mParentHelper = new NestedScrollingParentHelper(this); mChildHelper = new NestedScrollingChildHelper(this); ViewCompat.setNestedScrollingEnabled(this, true); mHeaderView = onCreateHeaderView(); mContentView = onCreateContentView(); if (!(mContentView instanceof IQMUIContinuousNestedBottomView)) { throw new IllegalStateException("the view create by onCreateContentView() " + "should implement from IQMUIContinuousNestedBottomView"); } addView(mHeaderView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, getHeaderHeightLayoutParam())); addView(mContentView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mHeaderViewOffsetHelper = new QMUIViewOffsetHelper(mHeaderView); mContentViewOffsetHelper = new QMUIViewOffsetHelper(mContentView); mViewFlinger = new ViewFlinger(); } public View getHeaderView() { return mHeaderView; } public View getContentView() { return mContentView; } public int getOffsetCurrent() { return -mHeaderViewOffsetHelper.getTopAndBottomOffset(); } public int getOffsetRange() { return -getMiniOffset(); } private int getMiniOffset() { IQMUIContinuousNestedBottomView b = (IQMUIContinuousNestedBottomView) mContentView; int contentHeight = b.getContentHeight(); FrameLayout.LayoutParams headerLp = (LayoutParams) mHeaderView.getLayoutParams(); int minOffset = -mHeaderView.getHeight() - headerLp.bottomMargin + getHeaderStickyHeight(); if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { minOffset += mContentView.getHeight() - contentHeight; minOffset = Math.min(minOffset, 0); } return minOffset; } @Override public int getContentHeight() { IQMUIContinuousNestedBottomView b = (IQMUIContinuousNestedBottomView) mContentView; int bc = b.getContentHeight(); if (bc == IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL || bc > mContentView.getHeight()) { return IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL; } int bottomMargin = getContentBottomMargin(); if (bc + mHeaderView.getHeight() + bottomMargin > getHeight()) { return IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL; } return mHeaderView.getHeight() + bc + bottomMargin; } @NonNull protected abstract View onCreateHeaderView(); @NonNull protected abstract View onCreateContentView(); protected int getHeaderStickyHeight() { return 0; } protected int getHeaderHeightLayoutParam() { return ViewGroup.LayoutParams.WRAP_CONTENT; } protected int getContentBottomMargin() { return 0; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); mContentView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( heightSize - getHeaderStickyHeight() - getContentBottomMargin(), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mHeaderView.layout(0, 0, mHeaderView.getMeasuredWidth(), mHeaderView.getMeasuredHeight()); int contentTop = mHeaderView.getBottom(); mContentView.layout(0, contentTop, mContentView.getMeasuredWidth(), contentTop + mContentView.getMeasuredHeight()); mHeaderViewOffsetHelper.onViewLayout(); mContentViewOffsetHelper.onViewLayout(); postCheckLayout(); } public void postCheckLayout() { removeCallbacks(mCheckLayoutAction); post(mCheckLayoutAction); } public void checkLayout() { int offsetCurrent = getOffsetCurrent(); int offsetRange = getOffsetRange(); IQMUIContinuousNestedBottomView bottomView = (IQMUIContinuousNestedBottomView) mContentView; if (offsetCurrent < offsetRange && bottomView.getCurrentScroll() > 0) { bottomView.consumeScroll(Integer.MIN_VALUE); } } private int offsetBy(int dyUnConsumed) { int canConsume = 0; FrameLayout.LayoutParams headerLp = (LayoutParams) mHeaderView.getLayoutParams(); int minOffset = getMiniOffset(); if (dyUnConsumed > 0) { canConsume = Math.min(mHeaderView.getTop() - minOffset, dyUnConsumed); } else if (dyUnConsumed < 0) { canConsume = Math.max(mHeaderView.getTop() - headerLp.topMargin, dyUnConsumed); } if (canConsume != 0) { mHeaderViewOffsetHelper.setTopAndBottomOffset(mHeaderViewOffsetHelper.getTopAndBottomOffset() - canConsume); mContentViewOffsetHelper.setTopAndBottomOffset(mContentViewOffsetHelper.getTopAndBottomOffset() - canConsume); } mOnScrollNotifier.notify(-mHeaderViewOffsetHelper.getTopAndBottomOffset(), mHeaderView.getHeight() + ((IQMUIContinuousNestedBottomView) mContentView).getScrollOffsetRange()); return dyUnConsumed - canConsume; } @Override public void consumeScroll(int dy) { if (dy == Integer.MAX_VALUE) { offsetBy(dy); ((IQMUIContinuousNestedBottomView) mContentView).consumeScroll(Integer.MAX_VALUE); return; } else if (dy == Integer.MIN_VALUE) { ((IQMUIContinuousNestedBottomView) mContentView).consumeScroll(Integer.MIN_VALUE); offsetBy(dy); return; } ((IQMUIContinuousNestedBottomView) mContentView).consumeScroll(dy); } @Override public void smoothScrollYBy(int dy, int duration) { ((IQMUIContinuousNestedBottomView) mContentView).smoothScrollYBy(dy, duration); } @Override public void stopScroll() { ((IQMUIContinuousNestedBottomView) mContentView).stopScroll(); } @Override public int getCurrentScroll() { return -mHeaderViewOffsetHelper.getTopAndBottomOffset() + ((IQMUIContinuousNestedBottomView) mContentView).getCurrentScroll(); } @Override public int getScrollOffsetRange() { if (getContentHeight() != HEIGHT_IS_ENOUGH_TO_SCROLL) { return 0; } return mHeaderView.getHeight() - getHeaderStickyHeight() + ((IQMUIContinuousNestedBottomView) mContentView).getScrollOffsetRange(); } @Override public void injectScrollNotifier(final OnScrollNotifier notifier) { mOnScrollNotifier = notifier; if (mContentView instanceof IQMUIContinuousNestedBottomView) { ((IQMUIContinuousNestedBottomView) mContentView).injectScrollNotifier(new OnScrollNotifier() { @Override public void notify(int innerOffset, int innerRange) { notifier.notify(innerOffset - mHeaderView.getTop(), innerRange + mHeaderView.getHeight()); } @Override public void onScrollStateChange(View view, int newScrollState) { notifier.onScrollStateChange(view, newScrollState); } }); } } @Override public void saveScrollInfo(@NonNull Bundle bundle) { bundle.putInt(KEY_SCROLL_INFO_OFFSET, mHeaderViewOffsetHelper.getTopAndBottomOffset()); if (mContentView != null) { ((IQMUIContinuousNestedBottomView) mContentView).saveScrollInfo(bundle); } } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { int offset = bundle.getInt(KEY_SCROLL_INFO_OFFSET, 0); offset = QMUILangHelper.constrain(offset, getMiniOffset(), 0); mHeaderViewOffsetHelper.setTopAndBottomOffset(offset); mContentViewOffsetHelper.setTopAndBottomOffset(offset); if (mContentView != null) { ((IQMUIContinuousNestedBottomView) mContentView).restoreScrollInfo(bundle); } } // NestedScrollingChild2 @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { mChildHelper.stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return mChildHelper.hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); } @Override public void stopNestedScroll() { stopNestedScroll(ViewCompat.TYPE_TOUCH); } @Override public boolean hasNestedScrollingParent() { return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } // NestedScrollingParent2 @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mParentHelper.onNestedScrollAccepted(child, target, axes, type); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); } @Override public void onStopNestedScroll(@NonNull View target, int type) { mParentHelper.onStopNestedScroll(target, type); stopNestedScroll(type); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { int remain = offsetBy(dyUnconsumed); dispatchNestedScroll(0, dyUnconsumed - remain, 0, remain, null, type); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { dispatchNestedPreScroll(dx, dy, consumed, null, type); int unconsumed = dy - consumed[1]; if (unconsumed > 0) { consumed[1] += unconsumed - offsetBy(unconsumed); } } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); } @Override public void onStopNestedScroll(View target) { onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { mViewFlinger.fling((int) velocityY); return true; } return false; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } private boolean isPointInHeaderBounds(int x, int y) { QMUIViewHelper.getDescendantRect(this, mHeaderView, mTempRect); return mTempRect.contains(x, y); } private void ensureVelocityTracker() { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (touchSlop < 0) { touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } final int action = ev.getAction(); if (action == MotionEvent.ACTION_MOVE && isBeingDragged) { return true; } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mViewFlinger.stop(); isBeingDragged = false; final int x = (int) ev.getX(); final int y = (int) ev.getY(); if (isPointInHeaderBounds(x, y)) { lastMotionY = y; this.activePointerId = ev.getPointerId(0); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); } break; } case MotionEvent.ACTION_POINTER_DOWN: { final int actionIndex = ev.getActionIndex(); return actionIndex != 0 && !isPointInHeaderBounds((int) ev.getX(), (int) ev.getY()) && isPointInHeaderBounds((int) ev.getX(actionIndex), (int) ev.getY(actionIndex)); } case MotionEvent.ACTION_MOVE: { final int activePointerId = this.activePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - lastMotionY); if (yDiff > touchSlop) { isBeingDragged = true; lastMotionY = y; } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { isBeingDragged = false; this.activePointerId = INVALID_POINTER; stopNestedScroll(ViewCompat.TYPE_TOUCH); break; } } return isBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { if (touchSlop < 0) { touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNestedOffsetY = 0; } final MotionEvent vtev = MotionEvent.obtain(ev); vtev.offsetLocation(0, mNestedOffsetY); switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mViewFlinger.stop(); final int x = (int) ev.getX(); final int y = (int) ev.getY(); if (isPointInHeaderBounds(x, y)) { lastMotionY = y; activePointerId = ev.getPointerId(0); ensureVelocityTracker(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); } else { return false; } break; } case MotionEvent.ACTION_MOVE: { final int activePointerIndex = ev.findPointerIndex(activePointerId); if (activePointerIndex == -1) { return false; } final int y = (int) ev.getY(activePointerIndex); int dy = lastMotionY - y; if (!isBeingDragged && Math.abs(dy) > touchSlop) { isBeingDragged = true; if (dy > 0) { dy -= touchSlop; } else { dy += touchSlop; } } if (isBeingDragged) { lastMotionY = y; if (dy < 0 && ((IQMUIContinuousNestedBottomView) mContentView).getCurrentScroll() > 0) { // the content view can scroll up, prevent drag return true; } mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; if (dispatchNestedPreScroll(0, dy, mScrollConsumed, mScrollOffset)) { dy -= mScrollConsumed[1]; lastMotionY = y - mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedOffsetY += mScrollOffset[1]; } int unconsumed = offsetBy(dy); if (dispatchNestedScroll(0, dy - unconsumed, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { lastMotionY = y - mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedOffsetY += mScrollOffset[1]; } } break; } case MotionEvent.ACTION_UP: if (velocityTracker != null) { velocityTracker.addMovement(vtev); velocityTracker.computeCurrentVelocity(1000); int yvel = -(int) (velocityTracker.getYVelocity(activePointerId) + 0.5f); mViewFlinger.fling(yvel); } // $FALLTHROUGH case MotionEvent.ACTION_CANCEL: { isBeingDragged = false; activePointerId = INVALID_POINTER; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } stopNestedScroll(ViewCompat.TYPE_TOUCH); break; } } if (velocityTracker != null) { velocityTracker.addMovement(vtev); } vtev.recycle(); return true; } class ViewFlinger implements Runnable { private int mLastFlingY; OverScroller mOverScroller; Interpolator mInterpolator = QUNITIC_INTERPOLATOR; // When set to true, postOnAnimation callbacks are delayed until the run method completes private boolean mEatRunOnAnimationRequest = false; // Tracks if postAnimationCallback should be re-attached when it is done private boolean mReSchedulePostAnimationCallback = false; ViewFlinger() { mOverScroller = new OverScroller(getContext(), QUNITIC_INTERPOLATOR); } @Override public void run() { mReSchedulePostAnimationCallback = false; mEatRunOnAnimationRequest = true; // Keep a local reference so that if it is changed during onAnimation method, it won't // cause unexpected behaviors final OverScroller scroller = mOverScroller; if (scroller.computeScrollOffset()) { final int y = scroller.getCurrY(); int unconsumedY = y - mLastFlingY; mLastFlingY = y; IQMUIContinuousNestedBottomView bottomView = (IQMUIContinuousNestedBottomView) mContentView; boolean canScroll = unconsumedY <= 0 || bottomView.getCurrentScroll() < bottomView.getScrollOffsetRange(); if(canScroll){ if (!mChildHelper.hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); } consumeScroll(unconsumedY); postOnAnimation(); }else{ stop(); } } mEatRunOnAnimationRequest = false; if (mReSchedulePostAnimationCallback) { internalPostOnAnimation(); } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } } void postOnAnimation() { if (mEatRunOnAnimationRequest) { mReSchedulePostAnimationCallback = true; } else { internalPostOnAnimation(); } } private void internalPostOnAnimation() { removeCallbacks(this); ViewCompat.postOnAnimation(QMUIContinuousNestedBottomDelegateLayout.this, this); } public void fling(int velocityY) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); mLastFlingY = 0; // Because you can't define a custom interpolator for flinging, we should make sure we // reset ourselves back to the teh default interpolator in case a different call // changed our interpolator. if (mInterpolator != QUNITIC_INTERPOLATOR) { mInterpolator = QUNITIC_INTERPOLATOR; mOverScroller = new OverScroller(getContext(), QUNITIC_INTERPOLATOR); } mOverScroller.fling(0, 0, 0, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); postOnAnimation(); } public void stop() { removeCallbacks(this); mOverScroller.abortAnimation(); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; public class QMUIContinuousNestedBottomRecyclerView extends RecyclerView implements IQMUIContinuousNestedBottomView { public static final String KEY_SCROLL_INFO_POSITION = "@qmui_scroll_info_bottom_rv_pos"; public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_scroll_info_bottom_rv_offset"; private IQMUIContinuousNestedBottomView.OnScrollNotifier mOnScrollNotifier; private final int[] mScrollConsumed = new int[2]; public QMUIContinuousNestedBottomRecyclerView(@NonNull Context context) { super(context); init(); } public QMUIContinuousNestedBottomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public QMUIContinuousNestedBottomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { setVerticalScrollBarEnabled(false); addOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (mOnScrollNotifier != null) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { mOnScrollNotifier.onScrollStateChange(recyclerView, IQMUIContinuousNestedScrollCommon.SCROLL_STATE_IDLE); } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) { mOnScrollNotifier.onScrollStateChange(recyclerView, IQMUIContinuousNestedScrollCommon.SCROLL_STATE_SETTLING); } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { mOnScrollNotifier.onScrollStateChange(recyclerView, IQMUIContinuousNestedScrollCommon.SCROLL_STATE_DRAGGING); } } } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (mOnScrollNotifier != null) { mOnScrollNotifier.notify( recyclerView.computeVerticalScrollOffset(), Math.max(0, recyclerView.computeVerticalScrollRange() - recyclerView.getHeight())); } } }); } @Override public void consumeScroll(int yUnconsumed) { if (yUnconsumed == Integer.MIN_VALUE) { if(canScrollVertically(-1)){ scrollToPosition(0); } } else if (yUnconsumed == Integer.MAX_VALUE) { if(canScrollVertically(1)) { Adapter adapter = getAdapter(); if (adapter != null) { scrollToPosition(adapter.getItemCount() - 1); } } } else { boolean reStartNestedScroll = false; if (!hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { // the scrollBy use ViewCompat.TYPE_TOUCH to handle nested scroll... reStartNestedScroll = true; startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); // and scrollBy only call dispatchNestedScroll, not call dispatchNestedPreScroll mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; dispatchNestedPreScroll(0, yUnconsumed, mScrollConsumed, null, ViewCompat.TYPE_TOUCH); yUnconsumed -= mScrollConsumed[1]; } scrollBy(0, yUnconsumed); if (reStartNestedScroll) { stopNestedScroll(ViewCompat.TYPE_TOUCH); } } } @Override public int getContentHeight() { Adapter adapter = getAdapter(); if (adapter == null) { return 0; } LayoutManager layoutManager = getLayoutManager(); if (layoutManager == null) { return 0; } final int scrollRange = this.computeVerticalScrollRange(); if (scrollRange > getHeight()) { return HEIGHT_IS_ENOUGH_TO_SCROLL; } return scrollRange; } @Override public void injectScrollNotifier(OnScrollNotifier notifier) { mOnScrollNotifier = notifier; } @Override public int getCurrentScroll() { return computeVerticalScrollOffset(); } @Override public int getScrollOffsetRange() { return Math.max(0, computeVerticalScrollRange() - getHeight()); } @Override public void smoothScrollYBy(int dy, int duration) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); smoothScrollBy(0, dy, null); } @Override public void saveScrollInfo(@NonNull Bundle bundle) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager lm = (LinearLayoutManager) layoutManager; int pos = lm.findFirstVisibleItemPosition(); View firstView = lm.findViewByPosition(pos); int offset = firstView == null ? 0 : firstView.getTop(); bundle.putInt(KEY_SCROLL_INFO_POSITION, pos); bundle.putInt(KEY_SCROLL_INFO_OFFSET, offset); } } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { int pos = bundle.getInt(KEY_SCROLL_INFO_POSITION, 0); int offset = bundle.getInt(KEY_SCROLL_INFO_OFFSET, 0); ((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(pos, offset); if(mOnScrollNotifier != null){ mOnScrollNotifier.notify(getCurrentScroll(), getScrollOffsetRange()); } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.qmuiteam.qmui.util.QMUILangHelper; import java.util.ArrayList; import java.util.List; public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implements QMUIContinuousNestedTopAreaBehavior.Callback, QMUIDraggableScrollBar.Callback { public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_nested_scroll_layout_offset"; private IQMUIContinuousNestedTopView mTopView; private IQMUIContinuousNestedBottomView mBottomView; private QMUIContinuousNestedTopAreaBehavior mTopAreaBehavior; private QMUIContinuousNestedBottomAreaBehavior mBottomAreaBehavior; private List mOnScrollListeners = new ArrayList<>(); private Runnable mCheckLayoutAction = new Runnable() { @Override public void run() { checkLayout(); } }; private boolean mKeepBottomAreaStableWhenCheckLayout = false; private QMUIDraggableScrollBar mDraggableScrollBar; private boolean mEnableScrollBarFadeInOut = true; private boolean mIsDraggableScrollBarEnabled = false; private int mCurrentScrollState = IQMUIContinuousNestedScrollCommon.SCROLL_STATE_IDLE; private boolean mIsDismissDownEvent = false; private float mDismissDownY = 0; private int mTouchSlap = -1; public QMUIContinuousNestedScrollLayout(@NonNull Context context) { this(context, null); } public QMUIContinuousNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public QMUIContinuousNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private void ensureScrollBar() { if (mDraggableScrollBar == null) { mDraggableScrollBar = createScrollBar(getContext()); mDraggableScrollBar.setEnableFadeInAndOut(mEnableScrollBarFadeInOut); mDraggableScrollBar.setCallback(this); CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); lp.gravity = Gravity.RIGHT; addView(mDraggableScrollBar, lp); } } public void setDraggableScrollBarEnabled(boolean draggableScrollBarEnabled) { if(mIsDraggableScrollBarEnabled != draggableScrollBarEnabled){ mIsDraggableScrollBarEnabled = draggableScrollBarEnabled; if(mIsDraggableScrollBarEnabled && !mEnableScrollBarFadeInOut){ ensureScrollBar(); mDraggableScrollBar.setPercent(getCurrentScrollPercent()); mDraggableScrollBar.awakenScrollBar(); } if(mDraggableScrollBar != null){ mDraggableScrollBar.setVisibility(draggableScrollBarEnabled ? View.VISIBLE: View.GONE); } } } public void setEnableScrollBarFadeInOut(boolean enableScrollBarFadeInOut) { if(mEnableScrollBarFadeInOut != enableScrollBarFadeInOut){ mEnableScrollBarFadeInOut = enableScrollBarFadeInOut; if(mIsDraggableScrollBarEnabled && !mEnableScrollBarFadeInOut){ ensureScrollBar(); mDraggableScrollBar.setPercent(getCurrentScrollPercent()); mDraggableScrollBar.awakenScrollBar(); } if(mDraggableScrollBar != null){ mDraggableScrollBar.setEnableFadeInAndOut(enableScrollBarFadeInOut); mDraggableScrollBar.invalidate(); } } } protected QMUIDraggableScrollBar createScrollBar(Context context) { return new QMUIDraggableScrollBar(context); } @Override public void onDragStarted() { stopScroll(); } @Override public void onDragToPercent(float percent) { int targetScroll = (int) (getScrollRange() * percent); scrollBy(targetScroll - getCurrentScroll()); } @Override public void onDragEnd() { } public int getCurrentScroll() { int currentScroll = 0; if (mTopView != null) { currentScroll += mTopView.getCurrentScroll(); } currentScroll += getOffsetCurrent(); if (mBottomView != null) { currentScroll += mBottomView.getCurrentScroll(); } return currentScroll; } public int getScrollRange() { int totalRange = 0; if (mTopView != null) { totalRange += mTopView.getScrollOffsetRange(); } totalRange += getOffsetRange(); if (mBottomView != null) { totalRange += mBottomView.getScrollOffsetRange(); } return totalRange; } public float getCurrentScrollPercent() { int scrollRange = getScrollRange(); if (scrollRange == 0) { return 0; } return getCurrentScroll() * 1f / scrollRange; } public void addOnScrollListener(@NonNull OnScrollListener onScrollListener) { if (!mOnScrollListeners.contains(onScrollListener)) { mOnScrollListeners.add(onScrollListener); } } public void removeOnScrollListener(OnScrollListener onScrollListener) { mOnScrollListeners.remove(onScrollListener); } public void setKeepBottomAreaStableWhenCheckLayout(boolean keepBottomAreaStableWhenCheckLayout) { mKeepBottomAreaStableWhenCheckLayout = keepBottomAreaStableWhenCheckLayout; } public boolean isKeepBottomAreaStableWhenCheckLayout() { return mKeepBottomAreaStableWhenCheckLayout; } public void setTopAreaView(View topView, @Nullable LayoutParams layoutParams) { if (!(topView instanceof IQMUIContinuousNestedTopView)) { throw new IllegalStateException("topView must implement from IQMUIContinuousNestedTopView"); } if (mTopView != null) { removeView(((View) mTopView)); } mTopView = (IQMUIContinuousNestedTopView) topView; mTopView.injectScrollNotifier(new IQMUIContinuousNestedScrollCommon.OnScrollNotifier() { @Override public void notify(int innerOffset, int innerRange) { int offsetCurrent = mTopAreaBehavior == null ? 0 : -mTopAreaBehavior.getTopAndBottomOffset(); int bottomCurrent = mBottomView == null ? 0 : mBottomView.getCurrentScroll(); int bottomRange = mBottomView == null ? 0 : mBottomView.getScrollOffsetRange(); dispatchScroll(innerOffset, innerRange, offsetCurrent, getOffsetRange(), bottomCurrent, bottomRange); } @Override public void onScrollStateChange(View view, int newScrollState) { // not need this. top view scroll is driven by top behavior } }); if (layoutParams == null) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } Behavior behavior = layoutParams.getBehavior(); if (behavior instanceof QMUIContinuousNestedTopAreaBehavior) { mTopAreaBehavior = (QMUIContinuousNestedTopAreaBehavior) behavior; } else { mTopAreaBehavior = new QMUIContinuousNestedTopAreaBehavior(getContext()); layoutParams.setBehavior(mTopAreaBehavior); } mTopAreaBehavior.setCallback(this); addView(topView, 0, layoutParams); } public IQMUIContinuousNestedTopView getTopView() { return mTopView; } public IQMUIContinuousNestedBottomView getBottomView() { return mBottomView; } public QMUIContinuousNestedTopAreaBehavior getTopAreaBehavior() { return mTopAreaBehavior; } public QMUIContinuousNestedBottomAreaBehavior getBottomAreaBehavior() { return mBottomAreaBehavior; } public void setBottomAreaView(View bottomView, @Nullable LayoutParams layoutParams) { if (!(bottomView instanceof IQMUIContinuousNestedBottomView)) { throw new IllegalStateException("bottomView must implement from IQMUIContinuousNestedBottomView"); } if (mBottomView != null) { removeView(((View) mBottomView)); } mBottomView = (IQMUIContinuousNestedBottomView) bottomView; mBottomView.injectScrollNotifier(new IQMUIContinuousNestedBottomView.OnScrollNotifier() { @Override public void notify(int innerOffset, int innerRange) { int topCurrent = mTopView == null ? 0 : mTopView.getCurrentScroll(); int topRange = mTopView == null ? 0 : mTopView.getScrollOffsetRange(); int offsetCurrent = mTopAreaBehavior == null ? 0 : -mTopAreaBehavior.getTopAndBottomOffset(); dispatchScroll(topCurrent, topRange, offsetCurrent, getOffsetRange(), innerOffset, innerRange); } @Override public void onScrollStateChange(View view, int newScrollState) { dispatchScrollStateChange(newScrollState, false); } }); if (layoutParams == null) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } Behavior behavior = layoutParams.getBehavior(); if (behavior instanceof QMUIContinuousNestedBottomAreaBehavior) { mBottomAreaBehavior = (QMUIContinuousNestedBottomAreaBehavior) behavior; } else { mBottomAreaBehavior = new QMUIContinuousNestedBottomAreaBehavior(); layoutParams.setBehavior(mBottomAreaBehavior); } addView(bottomView, 0, layoutParams); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); postCheckLayout(); } public void postCheckLayout() { removeCallbacks(mCheckLayoutAction); post(mCheckLayoutAction); } public void checkLayout() { if (mTopView == null || mBottomView == null) { return; } int topCurrent = mTopView.getCurrentScroll(); int topRange = mTopView.getScrollOffsetRange(); int offsetCurrent = -mTopAreaBehavior.getTopAndBottomOffset(); int offsetRange = getOffsetRange(); if (offsetRange <= 0) { return; } if (offsetCurrent >= offsetRange || (offsetCurrent > 0 && mKeepBottomAreaStableWhenCheckLayout)) { mTopView.consumeScroll(Integer.MAX_VALUE); if(mBottomView.getCurrentScroll() > 0){ mTopAreaBehavior.setTopAndBottomOffset(-offsetRange); } return; } if (mBottomView.getCurrentScroll() > 0) { mBottomView.consumeScroll(Integer.MIN_VALUE); } if (topCurrent < topRange && offsetCurrent > 0) { int remain = topRange - topCurrent; if (offsetCurrent >= remain) { mTopView.consumeScroll(Integer.MAX_VALUE); mTopAreaBehavior.setTopAndBottomOffset(remain - offsetCurrent); } else { mTopView.consumeScroll(offsetCurrent); mTopAreaBehavior.setTopAndBottomOffset(0); } } } public void scrollBottomViewToTop() { if (mTopView != null) { mTopView.consumeScroll(Integer.MAX_VALUE); } if (mBottomView != null) { mBottomView.consumeScroll(Integer.MIN_VALUE); int contentHeight = mBottomView.getContentHeight(); if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { mTopAreaBehavior.setTopAndBottomOffset(Math.min(0, getHeight() - contentHeight - ((View) mTopView).getHeight())); } else { mTopAreaBehavior.setTopAndBottomOffset( getHeight() - ((View) mBottomView).getHeight() - ((View) mTopView).getHeight()); } } } private void dispatchScroll(int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange) { if (mIsDraggableScrollBarEnabled) { ensureScrollBar(); mDraggableScrollBar.setPercent(getCurrentScrollPercent()); mDraggableScrollBar.awakenScrollBar(); } for (OnScrollListener onScrollListener : mOnScrollListeners) { onScrollListener.onScroll(this, topCurrent, topRange, offsetCurrent, offsetRange, bottomCurrent, bottomRange); } } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); if(dyUnconsumed > 0 && getCurrentScroll() >= getScrollRange()){ // RecyclerView does not stop scroller when over scroll with NestedScrollingParent stopScroll(); } } private void dispatchScrollStateChange(int newScrollState, boolean fromTopBehavior) { for (OnScrollListener onScrollListener : mOnScrollListeners) { onScrollListener.onScrollStateChange(this, newScrollState, fromTopBehavior); } mCurrentScrollState = newScrollState; } public void scrollBy(int dy) { if ((dy > 0 || mBottomView == null) && mTopAreaBehavior != null) { mTopAreaBehavior.scroll(this, ((View) mTopView), dy); } else if (dy != 0 && mBottomView != null) { mBottomView.consumeScroll(dy); } } public void smoothScrollBy(int dy, int duration) { if (dy == 0) { return; } if ((dy > 0 || mBottomView == null) && mTopAreaBehavior != null) { mTopAreaBehavior.smoothScrollBy(this, ((View) mTopView), dy, duration); } else if (mBottomView != null) { mBottomView.smoothScrollYBy(dy, duration); } } public void stopScroll() { if (mBottomView != null) { mBottomView.stopScroll(); } if (mTopAreaBehavior != null) { mTopAreaBehavior.stopFlingOrScroll(); } } public void scrollToTop() { if (mBottomView != null) { mBottomView.consumeScroll(Integer.MIN_VALUE); } if (mTopView != null) { mTopAreaBehavior.setTopAndBottomOffset(0); mTopView.consumeScroll(Integer.MIN_VALUE); } } public void scrollToBottom() { if (mTopView != null) { // consume the max value mTopView.consumeScroll(Integer.MAX_VALUE); if (mBottomView != null) { int contentHeight = mBottomView.getContentHeight(); if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { // bottomView can not scroll View topView = (View) mTopView; if (topView.getHeight() + contentHeight < getHeight()) { mTopAreaBehavior.setTopAndBottomOffset(0); } else { mTopAreaBehavior.setTopAndBottomOffset( getHeight() - contentHeight - ((View) mTopView).getHeight()); } } else { mTopAreaBehavior.setTopAndBottomOffset( getHeight() - ((View) mBottomView).getHeight() - ((View) mTopView).getHeight()); } } } if (mBottomView != null) { mBottomView.consumeScroll(Integer.MAX_VALUE); } } public int getOffsetCurrent() { return mTopAreaBehavior == null ? 0 : -mTopAreaBehavior.getTopAndBottomOffset(); } public int getOffsetRange() { if (mTopView == null && mBottomView == null) { return 0; } if(mBottomView == null){ return Math.max(0, ((View) mTopView).getHeight() - getHeight()); } if(mTopView == null){ return 0; } int contentHeight = mBottomView.getContentHeight(); if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { return Math.max(0, ((View) mTopView).getHeight() + contentHeight - getHeight()); } return Math.max(0, ((View) mTopView).getHeight() + ((View) mBottomView).getHeight() - getHeight()); } @Override public void onTopAreaOffset(int offset) { int topCurrent = mTopView == null ? 0 : mTopView.getCurrentScroll(); int topRange = mTopView == null ? 0 : mTopView.getScrollOffsetRange(); int bottomCurrent = mBottomView == null ? 0 : mBottomView.getCurrentScroll(); int bottomRange = mBottomView == null ? 0 : mBottomView.getScrollOffsetRange(); dispatchScroll(topCurrent, topRange, -offset, getOffsetRange(), bottomCurrent, bottomRange); } @Override public void onTopBehaviorTouchBegin() { dispatchScrollStateChange( IQMUIContinuousNestedScrollCommon.SCROLL_STATE_DRAGGING, true); } @Override public void onTopBehaviorTouchEnd() { dispatchScrollStateChange( IQMUIContinuousNestedScrollCommon.SCROLL_STATE_IDLE, true); } @Override public void onTopBehaviorFlingOrScrollStart() { dispatchScrollStateChange( IQMUIContinuousNestedScrollCommon.SCROLL_STATE_SETTLING, true); } @Override public void onTopBehaviorFlingOrScrollEnd() { dispatchScrollStateChange( IQMUIContinuousNestedScrollCommon.SCROLL_STATE_IDLE, true); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { if(mCurrentScrollState != IQMUIContinuousNestedScrollCommon.SCROLL_STATE_IDLE){ // must stop scroll and not use the current down event. // this is worked when topView scroll to bottomView or bottomView scroll to topView. stopScroll(); mIsDismissDownEvent = true; mDismissDownY = ev.getY(); if(mTouchSlap < 0){ mTouchSlap = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } return true; } } else if(ev.getAction() == MotionEvent.ACTION_MOVE && mIsDismissDownEvent){ if(Math.abs(ev.getY() - mDismissDownY) > mTouchSlap){ MotionEvent down = MotionEvent.obtain(ev); down.setAction(MotionEvent.ACTION_DOWN); down.offsetLocation(0, mDismissDownY - ev.getY()); super.dispatchTouchEvent(down); down.recycle(); }else{ return true; } } mIsDismissDownEvent = false; return super.dispatchTouchEvent(ev); } /** * save current scroll info to bundle * * @param bundle */ public void saveScrollInfo(@NonNull Bundle bundle) { if (mTopView != null) { mTopView.saveScrollInfo(bundle); } if (mBottomView != null) { mBottomView.saveScrollInfo(bundle); } bundle.putInt(KEY_SCROLL_INFO_OFFSET, getOffsetCurrent()); } /** * restore current scroll info from bundle * * @param bundle */ public void restoreScrollInfo(@Nullable Bundle bundle) { if (bundle == null) { return; } if (mTopAreaBehavior != null) { int offset = bundle.getInt(KEY_SCROLL_INFO_OFFSET, 0); mTopAreaBehavior.setTopAndBottomOffset(QMUILangHelper.constrain(-offset, -getOffsetRange(), 0)); } if (mTopView != null) { mTopView.restoreScrollInfo(bundle); } if (mBottomView != null) { mBottomView.restoreScrollInfo(bundle); } } public interface OnScrollListener { void onScroll(QMUIContinuousNestedScrollLayout scrollLayout, int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange); void onScrollStateChange(QMUIContinuousNestedScrollLayout scrollLayout, int newScrollState, boolean fromTopBehavior); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopAreaBehavior.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.nestedScroll; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.webkit.WebView; import android.widget.OverScroller; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.ViewCompat; import static android.view.View.MEASURED_SIZE_MASK; import static com.qmuiteam.qmui.QMUIInterpolatorStaticHolder.QUNITIC_INTERPOLATOR; public class QMUIContinuousNestedTopAreaBehavior extends QMUIViewOffsetBehavior { private static final int INVALID_POINTER = -1; private final ViewFlinger mViewFlinger; private final int[] mScrollConsumed = new int[2]; private boolean isBeingDragged; private int activePointerId = INVALID_POINTER; private int lastMotionY; private int touchSlop = -1; private VelocityTracker velocityTracker; private Callback mCallback; private boolean isInTouch = false; private boolean isInFlingOrScroll = false; private boolean replaceCancelActionWithMoveActionForWebView = true; public QMUIContinuousNestedTopAreaBehavior(Context context) { this(context, null); } public QMUIContinuousNestedTopAreaBehavior(Context context, AttributeSet attrs) { super(context, attrs); mViewFlinger = new ViewFlinger(context); } public void setReplaceCancelActionWithMoveActionForWebView(boolean replaceCancelActionWithMoveActionForWebView) { this.replaceCancelActionWithMoveActionForWebView = replaceCancelActionWithMoveActionForWebView; } public void setCallback(Callback callback) { mCallback = callback; } @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) { if (touchSlop < 0) { touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop(); } final int action = ev.getAction(); if (action == MotionEvent.ACTION_MOVE && isBeingDragged) { return true; } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mViewFlinger.stop(); isInTouch = true; isBeingDragged = false; final int x = (int) ev.getX(); final int y = (int) ev.getY(); if (parent.isPointInChildBounds(child, x, y)) { lastMotionY = y; this.activePointerId = ev.getPointerId(0); ensureVelocityTracker(); } break; } case MotionEvent.ACTION_POINTER_DOWN: { final int actionIndex = ev.getActionIndex(); return actionIndex != 0 && !parent.isPointInChildBounds(child, (int) ev.getX(), (int) ev.getY()) && parent.isPointInChildBounds( child, (int) ev.getX(actionIndex), (int) ev.getY(actionIndex)); } case MotionEvent.ACTION_MOVE: { final int activePointerId = this.activePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - lastMotionY); if (yDiff > touchSlop) { isBeingDragged = true; if(child instanceof WebView || child instanceof QMUIContinuousNestedTopDelegateLayout){ // dispatch cancel event not work in webView sometimes. MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.offsetLocation(-child.getLeft(), -child.getTop()); if(replaceCancelActionWithMoveActionForWebView){ cancelEvent.setAction(MotionEvent.ACTION_MOVE); }else{ cancelEvent.setAction(MotionEvent.ACTION_CANCEL); } child.dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } lastMotionY = y; if (mCallback != null) { mCallback.onTopBehaviorTouchBegin(); } } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { isInTouch = false; isBeingDragged = false; this.activePointerId = INVALID_POINTER; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } break; } } if (velocityTracker != null) { velocityTracker.addMovement(ev); } return isBeingDragged; } @Override public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) { if (touchSlop < 0) { touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop(); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mViewFlinger.stop(); isInTouch = true; final int x = (int) ev.getX(); final int y = (int) ev.getY(); if (parent.isPointInChildBounds(child, x, y)) { lastMotionY = y; activePointerId = ev.getPointerId(0); ensureVelocityTracker(); } else { return false; } break; } case MotionEvent.ACTION_MOVE: { final int activePointerIndex = ev.findPointerIndex(activePointerId); if (activePointerIndex == -1) { return false; } final int y = (int) ev.getY(activePointerIndex); int dy = lastMotionY - y; if (!isBeingDragged && Math.abs(dy) > touchSlop) { isBeingDragged = true; if (mCallback != null) { mCallback.onTopBehaviorTouchBegin(); } if (dy > 0) { dy -= touchSlop; } else { dy += touchSlop; } } if (isBeingDragged) { lastMotionY = y; scroll(parent, child, dy); } break; } case MotionEvent.ACTION_UP: isInTouch = false; if (mCallback != null) { mCallback.onTopBehaviorTouchEnd(); } if (velocityTracker != null) { velocityTracker.addMovement(ev); velocityTracker.computeCurrentVelocity(1000); int yvel = -(int) (velocityTracker.getYVelocity(activePointerId) + 0.5f); mViewFlinger.fling(parent, child, yvel); } // $FALLTHROUGH case MotionEvent.ACTION_CANCEL: { if (isInTouch) { isInTouch = false; if (mCallback != null) { mCallback.onTopBehaviorTouchEnd(); } } isBeingDragged = false; activePointerId = INVALID_POINTER; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } break; } } if (velocityTracker != null) { velocityTracker.addMovement(ev); } return true; } void scroll(@NonNull CoordinatorLayout parent, @NonNull View child, int dy) { mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; onNestedPreScroll(parent, child, child, 0, dy, mScrollConsumed, ViewCompat.TYPE_TOUCH); int unConsumed = dy - mScrollConsumed[1]; if (child instanceof IQMUIContinuousNestedTopView) { unConsumed = ((IQMUIContinuousNestedTopView) child).consumeScroll(unConsumed); } onNestedScroll(parent, child, child, 0, dy - unConsumed, 0, unConsumed, ViewCompat.TYPE_TOUCH); } void smoothScrollBy(@NonNull CoordinatorLayout parent, @NonNull View child, int dy, int duration) { mViewFlinger.startScroll(parent, child, dy, duration); } void stopFlingOrScroll() { mViewFlinger.stop(); } private void ensureVelocityTracker() { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } } @Override public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final int childLpHeight = child.getLayoutParams().height; int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec); if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT) { if (availableHeight == 0) { // If the measure spec doesn't specify a size, use the current height availableHeight = parent.getHeight(); } final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(availableHeight, View.MeasureSpec.AT_MOST); parent.onMeasureChild( child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); } else { parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, View.MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, View.MeasureSpec.AT_MOST), heightUsed); } return true; } @Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { boolean ret = super.onLayoutChild(parent, child, layoutDirection); int top = child.getTop(); int layoutTop = getLayoutTop(); if(top > layoutTop){ setTopAndBottomOffset(0); }else if(child.getBottom() < layoutTop + child.getHeight()){ setTopAndBottomOffset(-child.getHeight()); } return ret; } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { if (target.getParent() != parent) { return; } if (target == child) { // both target view and child view is top view if (dy < 0) { if (child.getTop() <= dy) { setTopAndBottomOffset(child.getTop() - dy - getLayoutTop()); consumed[1] += dy; } else if (child.getTop() < 0) { int top = child.getTop(); setTopAndBottomOffset(0 - getLayoutTop()); consumed[1] += top; } } } else { if (dy > 0) { // child is topView, target is bottomView if (target instanceof IQMUIContinuousNestedBottomView) { int contentHeight = ((IQMUIContinuousNestedBottomView) target).getContentHeight(); int minOffset; if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { minOffset = parent.getHeight() - contentHeight - child.getHeight(); } else { minOffset = parent.getHeight() - child.getHeight() - target.getHeight(); } if (child.getTop() - dy >= minOffset) { setTopAndBottomOffset(child.getTop() - dy - getLayoutTop()); consumed[1] += dy; } else if (child.getTop() > minOffset) { int distance = child.getTop() - minOffset; setTopAndBottomOffset(minOffset); consumed[1] += distance; } } } } } @Override public void onNestedScroll(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { if (target.getParent() != parent) { return; } if (target == child) { // both target view and child view is top view if (dyUnconsumed > 0) { View bottomView = findBottomView(parent); if (bottomView == null || bottomView.getVisibility() == View.GONE) { int parentBottom = parent.getHeight(); if (target.getBottom() - parentBottom >= dyUnconsumed) { setTopAndBottomOffset(target.getTop() - dyUnconsumed - getLayoutTop()); } else if (target.getBottom() - parentBottom > 0) { int moveDistance = target.getBottom() - parentBottom; setTopAndBottomOffset(target.getTop() - moveDistance - getLayoutTop()); } } else { int contentHeight = ((IQMUIContinuousNestedBottomView) bottomView).getContentHeight(); int minBottom = parent.getHeight(); boolean canContentScroll = true; if (contentHeight != IQMUIContinuousNestedBottomView.HEIGHT_IS_ENOUGH_TO_SCROLL) { minBottom = parent.getHeight() + bottomView.getHeight() - contentHeight; canContentScroll = false; } if (bottomView.getBottom() - minBottom > dyUnconsumed) { setTopAndBottomOffset(target.getTop() - dyUnconsumed - getLayoutTop()); return; } else if (bottomView.getBottom() - minBottom > 0) { int moveDistance = bottomView.getBottom() - minBottom; setTopAndBottomOffset(target.getTop() - moveDistance - getLayoutTop()); dyUnconsumed = dyUnconsumed == Integer.MAX_VALUE ? dyUnconsumed : (dyUnconsumed - moveDistance); } if (canContentScroll) { ((IQMUIContinuousNestedBottomView) bottomView).consumeScroll(dyUnconsumed); } } } } else { // child is topView, target is bottomView if (dyUnconsumed < 0) { if (child.getTop() <= dyUnconsumed) { setTopAndBottomOffset(child.getTop() - dyUnconsumed - getLayoutTop()); return; } else if (child.getTop() < 0) { int top = child.getTop(); setTopAndBottomOffset(0 - getLayoutTop()); dyUnconsumed = dyUnconsumed == Integer.MIN_VALUE ? dyConsumed : (dyUnconsumed - top); } if (child instanceof IQMUIContinuousNestedTopView) { ((IQMUIContinuousNestedTopView) child).consumeScroll(dyUnconsumed); } } } } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } private View findBottomView(CoordinatorLayout parent) { for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); if (child instanceof IQMUIContinuousNestedBottomView) { return child; } } return null; } class ViewFlinger implements Runnable { private int mLastFlingY; OverScroller mOverScroller; Interpolator mInterpolator = QUNITIC_INTERPOLATOR; // When set to true, postOnAnimation callbacks are delayed until the run method completes private boolean mEatRunOnAnimationRequest = false; // Tracks if postAnimationCallback should be re-attached when it is done private boolean mReSchedulePostAnimationCallback = false; private CoordinatorLayout mCurrentParent; private View mCurrentChild; ViewFlinger(Context context) { mOverScroller = new OverScroller(context, QUNITIC_INTERPOLATOR); } @Override public void run() { mReSchedulePostAnimationCallback = false; mEatRunOnAnimationRequest = true; // Keep a local reference so that if it is changed during onAnimation method, it won't // cause unexpected behaviors final OverScroller scroller = mOverScroller; if (scroller.computeScrollOffset()) { final int y = scroller.getCurrY(); int unconsumedY = y - mLastFlingY; mLastFlingY = y; if (mCurrentParent != null && mCurrentChild != null) { boolean canScroll = true; if(mCurrentParent instanceof QMUIContinuousNestedScrollLayout){ QMUIContinuousNestedScrollLayout layout = (QMUIContinuousNestedScrollLayout) mCurrentParent; if(unconsumedY > 0 && layout.getCurrentScroll() >= layout.getScrollRange()){ canScroll = false; }else if(unconsumedY < 0 && layout.getCurrentScroll() <= 0){ canScroll = false; } } if(canScroll){ scroll(mCurrentParent, mCurrentChild, unconsumedY); postOnAnimation(); }else{ mOverScroller.abortAnimation(); } } } mEatRunOnAnimationRequest = false; if (mReSchedulePostAnimationCallback) { internalPostOnAnimation(); } else { mCurrentParent = null; mCurrentChild = null; onFlingOrScrollEnd(); } } void postOnAnimation() { if (mEatRunOnAnimationRequest) { mReSchedulePostAnimationCallback = true; } else { internalPostOnAnimation(); } } private void internalPostOnAnimation() { if (mCurrentChild != null) { mCurrentParent.removeCallbacks(this); ViewCompat.postOnAnimation(mCurrentChild, this); } } public void fling(CoordinatorLayout parent, View child, int velocityY) { onFlingOrScrollStart(parent, child); mOverScroller.fling(0, 0, 0, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); postOnAnimation(); } public void startScroll(CoordinatorLayout parent, View child, int dy, int duration) { onFlingOrScrollStart(parent, child); mOverScroller.startScroll(0, 0, 0, dy, duration); postOnAnimation(); } private void onFlingOrScrollStart(CoordinatorLayout parent, View child) { isInFlingOrScroll = true; if (mCallback != null) { mCallback.onTopBehaviorFlingOrScrollStart(); } mCurrentParent = parent; mCurrentChild = child; mLastFlingY = 0; // Because you can't define a custom interpolator for flinging, we should make sure we // reset ourselves back to the teh default interpolator in case a different call // changed our interpolator. if (mInterpolator != QUNITIC_INTERPOLATOR) { mInterpolator = QUNITIC_INTERPOLATOR; mOverScroller = new OverScroller(mCurrentParent.getContext(), QUNITIC_INTERPOLATOR); } } public void stop() { if (mCurrentChild != null) { mCurrentChild.removeCallbacks(this); } mOverScroller.abortAnimation(); mCurrentChild = null; mCurrentParent = null; onFlingOrScrollEnd(); } private void onFlingOrScrollEnd() { if (mCallback != null && isInFlingOrScroll) { mCallback.onTopBehaviorFlingOrScrollEnd(); } isInFlingOrScroll = false; } } @Override public boolean setTopAndBottomOffset(int offset) { boolean ret = super.setTopAndBottomOffset(offset); if (mCallback != null) { mCallback.onTopAreaOffset(offset); } return ret; } public interface Callback { void onTopAreaOffset(int offset); void onTopBehaviorTouchBegin(); void onTopBehaviorTouchEnd(); void onTopBehaviorFlingOrScrollStart(); void onTopBehaviorFlingOrScrollEnd(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopDelegateLayout.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.NestedScrollingChild2; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.NestedScrollingParent2; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ViewCompat; public class QMUIContinuousNestedTopDelegateLayout extends FrameLayout implements NestedScrollingChild2, NestedScrollingParent2, IQMUIContinuousNestedTopView { public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_scroll_info_top_dl_offset"; private OnScrollNotifier mScrollNotifier; private View mHeaderView; private IQMUIContinuousNestedTopView mDelegateView; private View mFooterView; private QMUIViewOffsetHelper mHeaderViewOffsetHelper; private QMUIViewOffsetHelper mDelegateViewOffsetHelper; private QMUIViewOffsetHelper mFooterViewOffsetHelper; private int mOffsetCurrent = 0; private int mOffsetRange = 0; private final NestedScrollingParentHelper mParentHelper; private final NestedScrollingChildHelper mChildHelper; private Runnable mCheckLayoutAction = new Runnable() { @Override public void run() { checkLayout(); } }; public QMUIContinuousNestedTopDelegateLayout(@NonNull Context context) { this(context, null); } public QMUIContinuousNestedTopDelegateLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public QMUIContinuousNestedTopDelegateLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mParentHelper = new NestedScrollingParentHelper(this); mChildHelper = new NestedScrollingChildHelper(this); ViewCompat.setNestedScrollingEnabled(this, true); setClipToPadding(false); } public void setHeaderView(@NonNull View headerView) { mHeaderView = headerView; mHeaderViewOffsetHelper = new QMUIViewOffsetHelper(headerView); addView(headerView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); } public void setDelegateView(@NonNull IQMUIContinuousNestedTopView delegateView) { if (!(delegateView instanceof View)) { throw new IllegalArgumentException("delegateView must be a instance of View"); } if (mDelegateView != null) { mDelegateView.injectScrollNotifier(null); } mDelegateView = delegateView; View view = (View) delegateView; mDelegateViewOffsetHelper = new QMUIViewOffsetHelper(view); // WRAP_CONTENT, the height will be handled by QMUIContinuousNestedTopAreaBehavior addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } public void setFooterView(@NonNull View footerView) { mFooterView = footerView; mFooterViewOffsetHelper = new QMUIViewOffsetHelper(footerView); addView(footerView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); int anchorHeight = getPaddingTop(); if (mHeaderView != null) { mHeaderView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.UNSPECIFIED)); anchorHeight += mHeaderView.getMeasuredHeight(); } if (mDelegateView != null) { View delegateView = (View) mDelegateView; delegateView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.AT_MOST)); anchorHeight += delegateView.getMeasuredHeight(); } if (mFooterView != null) { mFooterView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.UNSPECIFIED)); anchorHeight += mFooterView.getMeasuredHeight(); } anchorHeight += getPaddingBottom(); if (anchorHeight < h) { setMeasuredDimension(w, anchorHeight); } else { setMeasuredDimension(w, h); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int w = right - left, h = bottom - top; int anchorTop = getPaddingTop(); int viewHeight; if (mHeaderView != null) { viewHeight = mHeaderView.getMeasuredHeight(); mHeaderView.layout(0, anchorTop, w, anchorTop + viewHeight); anchorTop += viewHeight; } if (mDelegateView != null) { View view = (View) mDelegateView; viewHeight = view.getMeasuredHeight(); view.layout(0, anchorTop, w, anchorTop + viewHeight); anchorTop += viewHeight; } if (mFooterView != null) { viewHeight = mFooterView.getMeasuredHeight(); mFooterView.layout(0, anchorTop, w, anchorTop + viewHeight); anchorTop += viewHeight; } anchorTop += getPaddingBottom(); mOffsetRange = Math.max(0, anchorTop - h); if (mHeaderViewOffsetHelper != null) { mHeaderViewOffsetHelper.onViewLayout(); mOffsetCurrent = -mHeaderViewOffsetHelper.getTopAndBottomOffset(); } if (mDelegateViewOffsetHelper != null) { mDelegateViewOffsetHelper.onViewLayout(); mOffsetCurrent = -mDelegateViewOffsetHelper.getTopAndBottomOffset(); } if (mFooterViewOffsetHelper != null) { mFooterViewOffsetHelper.onViewLayout(); mOffsetCurrent = -mFooterViewOffsetHelper.getTopAndBottomOffset(); } if(mOffsetCurrent > mOffsetRange){ offsetTo(mOffsetRange); } postCheckLayout(); } public void postCheckLayout() { removeCallbacks(mCheckLayoutAction); post(mCheckLayoutAction); } public void checkLayout() { if (mHeaderView == null && mFooterView == null) { return; } if (mDelegateView == null) { return; } int headerOffsetRange = getContainerHeaderOffsetRange(); int delegateCurrentScroll = mDelegateView.getCurrentScroll(); int delegateScrollRange = mDelegateView.getScrollOffsetRange(); if (delegateCurrentScroll > 0 && mHeaderView != null && mOffsetCurrent < headerOffsetRange) { int over = headerOffsetRange - mOffsetCurrent; if (over >= delegateCurrentScroll) { mDelegateView.consumeScroll(Integer.MIN_VALUE); offsetTo(mOffsetCurrent + delegateCurrentScroll); } else { mDelegateView.consumeScroll(-over); offsetTo(headerOffsetRange); } } if (mOffsetCurrent > headerOffsetRange && delegateCurrentScroll < delegateScrollRange && mFooterView != null) { int over = mOffsetCurrent - headerOffsetRange; int delegateRemain = delegateScrollRange - delegateCurrentScroll; if (over >= delegateRemain) { mDelegateView.consumeScroll(Integer.MAX_VALUE); offsetTo(headerOffsetRange + over - delegateRemain); } else { mDelegateView.consumeScroll(over); offsetTo(headerOffsetRange); } } } private void offsetTo(int targetOffsetCurrent) { mOffsetCurrent = targetOffsetCurrent; if (mHeaderViewOffsetHelper != null) { mHeaderViewOffsetHelper.setTopAndBottomOffset(-targetOffsetCurrent); } if (mDelegateViewOffsetHelper != null) { mDelegateViewOffsetHelper.setTopAndBottomOffset(-targetOffsetCurrent); } if (mFooterViewOffsetHelper != null) { mFooterViewOffsetHelper.setTopAndBottomOffset(-targetOffsetCurrent); } if (mScrollNotifier != null) { mScrollNotifier.notify(getCurrentScroll(), getScrollOffsetRange()); } } public IQMUIContinuousNestedTopView getDelegateView() { return mDelegateView; } public View getHeaderView() { return mHeaderView; } public View getFooterView() { return mFooterView; } public int getContainerOffsetCurrent() { return mOffsetCurrent; } public int getContainerOffsetRange() { return mOffsetRange; } public int getContainerHeaderOffsetRange() { if (mOffsetRange == 0 || mHeaderView == null) { return 0; } int maxHeight = getPaddingTop() + mHeaderView.getHeight(); return Math.min(maxHeight, mOffsetRange); } @Override public int consumeScroll(int dyUnconsumed) { if (mOffsetRange <= 0) { if (mDelegateView != null) { return mDelegateView.consumeScroll(dyUnconsumed); } return dyUnconsumed; } if (dyUnconsumed > 0) { if (mDelegateView == null) { if (dyUnconsumed == Integer.MAX_VALUE) { offsetTo(mOffsetRange); } else if (mOffsetCurrent + dyUnconsumed <= mOffsetRange) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else if (mOffsetCurrent < mOffsetRange) { dyUnconsumed -= mOffsetRange - mOffsetCurrent; offsetTo(mOffsetRange); } return dyUnconsumed; } else { int beforeRange = Math.min(mOffsetRange, getPaddingTop() + (mHeaderView == null ? 0 : mHeaderView.getHeight())); if (dyUnconsumed == Integer.MAX_VALUE) { offsetTo(beforeRange); } else if (mOffsetCurrent + dyUnconsumed <= beforeRange) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else if (mOffsetCurrent < beforeRange) { dyUnconsumed -= beforeRange - mOffsetCurrent; offsetTo(beforeRange); } dyUnconsumed = mDelegateView.consumeScroll(dyUnconsumed); if (dyUnconsumed <= 0) { return dyUnconsumed; } if (dyUnconsumed == Integer.MAX_VALUE) { offsetTo(mOffsetRange); } else if (mOffsetCurrent + dyUnconsumed <= mOffsetRange) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else { dyUnconsumed -= mOffsetRange - mOffsetCurrent; offsetTo(mOffsetRange); return dyUnconsumed; } } } else if (dyUnconsumed < 0) { if (mDelegateView == null) { if (dyUnconsumed == Integer.MIN_VALUE) { offsetTo(0); } else if (mOffsetCurrent + dyUnconsumed >= 0) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else if (mOffsetCurrent > 0) { dyUnconsumed += mOffsetCurrent; offsetTo(0); } return dyUnconsumed; } int afterRange = Math.max(0, mOffsetRange - getPaddingBottom() - (mFooterView == null ? 0 : mFooterView.getHeight())); if (dyUnconsumed == Integer.MIN_VALUE) { offsetTo(afterRange); } else if (mOffsetCurrent + dyUnconsumed > afterRange) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else if (mOffsetCurrent > afterRange) { dyUnconsumed += mOffsetCurrent - afterRange; offsetTo(afterRange); } dyUnconsumed = mDelegateView.consumeScroll(dyUnconsumed); if (dyUnconsumed >= 0) { return dyUnconsumed; } if (dyUnconsumed == Integer.MIN_VALUE) { offsetTo(0); } else if (mOffsetCurrent + dyUnconsumed > 0) { offsetTo(mOffsetCurrent + dyUnconsumed); return 0; } else if (mOffsetCurrent > 0) { dyUnconsumed += mOffsetCurrent; offsetTo(0); } } return dyUnconsumed; } @Override public int getCurrentScroll() { int currentOffset = mOffsetCurrent; if (mDelegateView != null) { currentOffset += mDelegateView.getCurrentScroll(); } return currentOffset; } @Override public int getScrollOffsetRange() { int scrollRange = mOffsetRange; if (mDelegateView != null) { scrollRange += mDelegateView.getScrollOffsetRange(); } return scrollRange; } @Override public void injectScrollNotifier(final OnScrollNotifier notifier) { mScrollNotifier = notifier; if (mDelegateView != null) { mDelegateView.injectScrollNotifier(new OnScrollNotifier() { @Override public void notify(int innerOffset, int innerRange) { notifier.notify(getCurrentScroll(), getScrollOffsetRange()); } @Override public void onScrollStateChange(View view, int newScrollState) { } }); } } @Override public void saveScrollInfo(@NonNull Bundle bundle) { bundle.putInt(KEY_SCROLL_INFO_OFFSET, -mOffsetCurrent); if (mDelegateView != null) { mDelegateView.saveScrollInfo(bundle); } } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { int offset = bundle.getInt(KEY_SCROLL_INFO_OFFSET, 0); offsetTo(QMUILangHelper.constrain(-offset, 0, getContainerOffsetRange())); if (mDelegateView != null) { mDelegateView.restoreScrollInfo(bundle); } } // NestedScrollingChild2 @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { mChildHelper.stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return mChildHelper.hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); } @Override public void stopNestedScroll() { stopNestedScroll(ViewCompat.TYPE_TOUCH); } @Override public boolean hasNestedScrollingParent() { return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } // NestedScrollingParent2 @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mParentHelper.onNestedScrollAccepted(child, target, axes, type); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); } @Override public void onStopNestedScroll(@NonNull View target, int type) { mParentHelper.onStopNestedScroll(target, type); stopNestedScroll(type); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { int consumed = 0; if (dyUnconsumed > 0) { if (mOffsetCurrent + dyUnconsumed <= mOffsetRange) { consumed = dyUnconsumed; offsetTo(mOffsetCurrent + dyUnconsumed); } else if (mOffsetCurrent <= mOffsetRange) { consumed = mOffsetRange - mOffsetCurrent; offsetTo(mOffsetRange); } } else if (dyUnconsumed < 0) { if (mOffsetCurrent + dyUnconsumed >= 0) { consumed = dyUnconsumed; offsetTo(mOffsetCurrent + dyUnconsumed); } else if (mOffsetCurrent >= 0) { consumed = -mOffsetCurrent; offsetTo(0); } } dispatchNestedScroll(0, dyConsumed + consumed, 0, dyUnconsumed - consumed, null, type); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { dispatchNestedPreScroll(dx, dy, consumed, null, type); int unconsumed = dy - consumed[1]; if (unconsumed > 0) { int topMargin = Math.min(mOffsetRange, getPaddingTop() + (mHeaderView == null ? 0 : mHeaderView.getHeight())); if (mOffsetCurrent + unconsumed <= topMargin) { offsetTo(mOffsetCurrent + unconsumed); consumed[1] += unconsumed; } else if (mOffsetCurrent < topMargin) { consumed[1] += topMargin - mOffsetCurrent; offsetTo(topMargin); } } else if (unconsumed < 0) { int bottomMargin = getPaddingBottom() + (mFooterView != null ? mFooterView.getHeight() : 0); if(mOffsetRange > bottomMargin){ int b = mOffsetRange - bottomMargin; if (mOffsetCurrent + unconsumed >= b) { offsetTo(mOffsetCurrent + unconsumed); consumed[1] += unconsumed; } else if (mOffsetCurrent > b) { consumed[1] += b - mOffsetCurrent; offsetTo(b); } } } } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); } @Override public void onStopNestedScroll(View target) { onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopLinearLayout.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import com.qmuiteam.qmui.layout.QMUILinearLayout; import androidx.annotation.NonNull; public class QMUIContinuousNestedTopLinearLayout extends QMUILinearLayout implements IQMUIContinuousNestedTopView { public QMUIContinuousNestedTopLinearLayout(Context context) { super(context); } public QMUIContinuousNestedTopLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIContinuousNestedTopLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public int consumeScroll(int dyUnconsumed) { return dyUnconsumed; } @Override public int getCurrentScroll() { return 0; } @Override public int getScrollOffsetRange() { return 0; } @Override public void injectScrollNotifier(OnScrollNotifier notifier) { } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { } @Override public void saveScrollInfo(@NonNull Bundle bundle) { } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; public class QMUIContinuousNestedTopRecyclerView extends RecyclerView implements IQMUIContinuousNestedTopView { public static final String KEY_SCROLL_INFO_POSITION = "@qmui_scroll_info_top_rv_pos"; public static final String KEY_SCROLL_INFO_OFFSET = "@qmui_scroll_info_top_rv_offset"; private OnScrollNotifier mScrollNotifier; private final int[] mScrollConsumed = new int[2]; public QMUIContinuousNestedTopRecyclerView(@NonNull Context context) { this(context, null); init(); } public QMUIContinuousNestedTopRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); init(); } public QMUIContinuousNestedTopRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init(){ setVerticalScrollBarEnabled(false); } @Override public int consumeScroll(int dyUnconsumed) { if (dyUnconsumed == Integer.MIN_VALUE) { if(canScrollVertically(-1)){ scrollToPosition(0); } return Integer.MIN_VALUE; } else if (dyUnconsumed == Integer.MAX_VALUE) { if(canScrollVertically(1)){ Adapter adapter = getAdapter(); if (adapter != null) { scrollToPosition(adapter.getItemCount() - 1); } } return Integer.MAX_VALUE; } boolean reStartNestedScroll = false; if (!hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { // the scrollBy use ViewCompat.TYPE_TOUCH to handle nested scroll... reStartNestedScroll = true; startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); // and scrollBy only call dispatchNestedScroll, not call dispatchNestedPreScroll mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; dispatchNestedPreScroll(0, dyUnconsumed, mScrollConsumed, null, ViewCompat.TYPE_TOUCH); dyUnconsumed -= mScrollConsumed[1]; } scrollBy(0, dyUnconsumed); if (reStartNestedScroll) { stopNestedScroll(ViewCompat.TYPE_TOUCH); } return 0; } @Override public int getCurrentScroll() { return computeVerticalScrollOffset(); } @Override public int getScrollOffsetRange() { return Math.max(0, computeVerticalScrollRange() - getHeight()); } @Override public void injectScrollNotifier(OnScrollNotifier notifier) { mScrollNotifier = notifier; } @Override public void onScrolled(int dx, int dy) { super.onScrolled(dx, dy); if(mScrollNotifier != null){ mScrollNotifier.notify(getCurrentScroll(), getScrollOffsetRange()); } } @Override public void saveScrollInfo(@NonNull Bundle bundle) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager lm = (LinearLayoutManager) layoutManager; int pos = lm.findFirstVisibleItemPosition(); View firstView = lm.findViewByPosition(pos); int offset = firstView == null ? 0 : firstView.getTop(); bundle.putInt(KEY_SCROLL_INFO_POSITION, pos); bundle.putInt(KEY_SCROLL_INFO_OFFSET, offset); } } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { int pos = bundle.getInt(KEY_SCROLL_INFO_POSITION, 0); int offset = bundle.getInt(KEY_SCROLL_INFO_OFFSET, 0); ((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(pos, offset); if(mScrollNotifier != null){ mScrollNotifier.notify(getCurrentScroll(), getScrollOffsetRange()); } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.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.nestedScroll; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import androidx.annotation.NonNull; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.webview.QMUIWebView; public class QMUIContinuousNestedTopWebView extends QMUIWebView implements IQMUIContinuousNestedTopView { public static final String KEY_SCROLL_INFO = "@qmui_scroll_info_top_webview"; private OnScrollNotifier mScrollNotifier; public QMUIContinuousNestedTopWebView(Context context) { super(context); init(); } public QMUIContinuousNestedTopWebView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public QMUIContinuousNestedTopWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ setVerticalScrollBarEnabled(false); } @Override public int consumeScroll(int yUnconsumed) { // compute the consumed value int scrollY = getScrollY(); int maxScrollY = getScrollOffsetRange(); // the scrollY may be negative or larger than scrolling range scrollY = Math.max(0, Math.min(scrollY, maxScrollY)); int dy = 0; if (yUnconsumed < 0) { dy = Math.max(yUnconsumed, -scrollY); } else if (yUnconsumed > 0) { dy = Math.min(yUnconsumed, maxScrollY - scrollY); } scrollBy(0, dy); return yUnconsumed - dy; } @Override public int getCurrentScroll() { int scrollY = getScrollY(); int scrollRange = getScrollOffsetRange(); return Math.max(0, Math.min(scrollY, scrollRange)); } @Override public int getScrollOffsetRange() { return computeVerticalScrollRange() - getHeight(); } @Override public void injectScrollNotifier(OnScrollNotifier notifier) { mScrollNotifier = notifier; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (mScrollNotifier != null) { mScrollNotifier.notify(getCurrentScroll(), getScrollOffsetRange()); } } @Override public void saveScrollInfo(@NonNull Bundle bundle) { bundle.putInt(KEY_SCROLL_INFO, getScrollY()); } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { int scrollY = QMUIDisplayHelper.px2dp(getContext(), bundle.getInt(KEY_SCROLL_INFO, 0)); exec("javascript:scrollTo(0, " + scrollY + ")"); } private void exec(final String jsCode) { evaluateJavascript(jsCode, null); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.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. */ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qmuiteam.qmui.nestedScroll; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUILangHelper; public class QMUIDraggableScrollBar extends View { private int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; private int[] STATE_NORMAL = new int[]{}; private Drawable mDragDrawable; private int mKeepShownTime = 800; private int mTransitionDuration = 100; private long mStartTransitionTime = 0; private float mCurrentAlpha = 0f; private float mPercent = 0f; private Runnable mDelayInvalidateRunnable = new Runnable() { @Override public void run() { invalidate(); } }; private boolean mIsInDragging = false; private Callback mCallback; private int mDrawableDrawTop = -1; private float mDragInnerTop = 0; private int mAdjustDistanceProtection = QMUIDisplayHelper.dp2px(getContext(), 20); private int mAdjustMaxDistanceOnce = QMUIDisplayHelper.dp2px(getContext(), 4); private boolean mAdjustDistanceWithAnimation = true; private boolean enableFadeInAndOut = true; public QMUIDraggableScrollBar(Context context) { this(context, (AttributeSet) null); } public QMUIDraggableScrollBar(Context context, @NonNull Drawable dragDrawable) { this(context, (AttributeSet) null); mDragDrawable = dragDrawable.mutate(); } public QMUIDraggableScrollBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setCallback(Callback callback) { mCallback = callback; } public void setAdjustDistanceWithAnimation(boolean adjustDistanceWithAnimation) { mAdjustDistanceWithAnimation = adjustDistanceWithAnimation; } public void setKeepShownTime(int keepShownTime) { mKeepShownTime = keepShownTime; } public void setTransitionDuration(int transitionDuration) { mTransitionDuration = transitionDuration; } public void setEnableFadeInAndOut(boolean enableFadeInAndOut) { this.enableFadeInAndOut = enableFadeInAndOut; } public boolean isEnableFadeInAndOut() { return enableFadeInAndOut; } public void setDragDrawable(Drawable dragDrawable) { mDragDrawable = dragDrawable.mutate(); invalidate(); } public void setPercent(float percent) { if(!mIsInDragging){ setPercentInternal(percent); } } private void setPercentInternal(float percent){ mPercent = percent; invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Drawable drawable = mDragDrawable; if (drawable == null) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } super.onMeasure(MeasureSpec.makeMeasureSpec( drawable.getIntrinsicWidth(), MeasureSpec.EXACTLY), heightMeasureSpec); } @Override public boolean onTouchEvent(MotionEvent event) { Drawable drawable = mDragDrawable; if (drawable == null) { return super.onTouchEvent(event); } int action = event.getAction(); final float x = event.getX(); final float y = event.getY(); if (action == MotionEvent.ACTION_DOWN) { mIsInDragging = false; if (mCurrentAlpha > 0 && x > getWidth() - drawable.getIntrinsicWidth() && y >= mDrawableDrawTop && y <= mDrawableDrawTop + drawable.getIntrinsicHeight()) { mDragInnerTop = y - mDrawableDrawTop; getParent().requestDisallowInterceptTouchEvent(true); mIsInDragging = true; if(mCallback != null){ mCallback.onDragStarted(); mDragDrawable.setState(STATE_PRESSED); } } } else if (action == MotionEvent.ACTION_MOVE) { if (mIsInDragging) { getParent().requestDisallowInterceptTouchEvent(true); onDragging(drawable, y); } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mIsInDragging) { mIsInDragging = false; onDragging(drawable, y); if(mCallback != null){ mCallback.onDragEnd(); mDragDrawable.setState(STATE_NORMAL); } } } return mIsInDragging; } private void onDragging(Drawable drawable, float currentY) { float percent = (currentY - getScrollBarTopMargin() - mDragInnerTop) / (getHeight() - getScrollBarBottomMargin() - getScrollBarTopMargin() - drawable.getIntrinsicHeight()); percent = QMUILangHelper.constrain(percent, 0f, 1f); if (mCallback != null) { mCallback.onDragToPercent(percent); } setPercentInternal(percent); } public void awakenScrollBar() { if (mDragDrawable == null) { mDragDrawable = ContextCompat.getDrawable(getContext(), R.drawable.qmui_icon_scroll_bar); } long current = System.currentTimeMillis(); if (current - mStartTransitionTime > mTransitionDuration) { mStartTransitionTime = current - mTransitionDuration; } ViewCompat.postInvalidateOnAnimation(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Drawable drawable = mDragDrawable; if (drawable == null) { return; } int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); if (drawableWidth <= 0 || drawableHeight <= 0) { return; } int needInvalidate = -1; if(enableFadeInAndOut){ long timeAfterStartShow = System.currentTimeMillis() - mStartTransitionTime; long timeAfterEndShow; if (timeAfterStartShow < mTransitionDuration) { // in show animation mCurrentAlpha = timeAfterStartShow * 1f / mTransitionDuration; needInvalidate = 0; } else if (timeAfterStartShow - mTransitionDuration < mKeepShownTime) { // keep show mCurrentAlpha = 1f; needInvalidate = (int) (mKeepShownTime - (timeAfterStartShow - mTransitionDuration)); } else if ((timeAfterEndShow = timeAfterStartShow - mTransitionDuration - mKeepShownTime) < mTransitionDuration) { // in hide animation mCurrentAlpha = 1 - timeAfterEndShow * 1f / mTransitionDuration; needInvalidate = 0; } else { mCurrentAlpha = 0f; } if (mCurrentAlpha <= 0f) { return; } }else{ mCurrentAlpha = 1f; } drawable.setAlpha((int) (mCurrentAlpha * 255)); int totalHeight = getHeight() - getScrollBarTopMargin() - getScrollBarBottomMargin(); int totalWidth = getWidth(); int top = getScrollBarTopMargin() + (int) ((totalHeight - drawableHeight) * mPercent); int left = totalWidth - drawableWidth; if (!mIsInDragging && mDrawableDrawTop > 0 && mAdjustDistanceWithAnimation) { int moveDistance = top - mDrawableDrawTop; if (moveDistance > mAdjustMaxDistanceOnce && moveDistance < mAdjustDistanceProtection) { top = mDrawableDrawTop + mAdjustMaxDistanceOnce; needInvalidate = 0; } else if (moveDistance < -mAdjustMaxDistanceOnce && moveDistance > -mAdjustDistanceProtection) { top = mDrawableDrawTop - mAdjustMaxDistanceOnce; needInvalidate = 0; } } drawable.setBounds(0, 0, drawableWidth, drawableHeight); canvas.save(); canvas.translate(left, top); drawable.draw(canvas); canvas.restore(); mDrawableDrawTop = top; if (needInvalidate == 0) { invalidate(); } else if (needInvalidate > 0) { ViewCompat.postOnAnimationDelayed(this, mDelayInvalidateRunnable, needInvalidate); } } protected int getScrollBarTopMargin() { return 0; } protected int getScrollBarBottomMargin() { return 0; } interface Callback { void onDragStarted(); void onDragToPercent(float percent); void onDragEnd(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIViewOffsetBehavior.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. */ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qmuiteam.qmui.nestedScroll; import android.content.Context; import android.util.AttributeSet; import android.view.View; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import androidx.coordinatorlayout.widget.CoordinatorLayout; public class QMUIViewOffsetBehavior extends CoordinatorLayout.Behavior { private QMUIViewOffsetHelper viewOffsetHelper; private int tempTopBottomOffset = 0; private int tempLeftRightOffset = 0; public QMUIViewOffsetBehavior() { } public QMUIViewOffsetBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { // First let lay the child out layoutChild(parent, child, layoutDirection); if (viewOffsetHelper == null) { viewOffsetHelper = new QMUIViewOffsetHelper(child); } viewOffsetHelper.onViewLayout(); if (tempTopBottomOffset != 0) { viewOffsetHelper.setTopAndBottomOffset(tempTopBottomOffset); tempTopBottomOffset = 0; } if (tempLeftRightOffset != 0) { viewOffsetHelper.setLeftAndRightOffset(tempLeftRightOffset); tempLeftRightOffset = 0; } return true; } protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) { // Let the parent lay it out by default parent.onLayoutChild(child, layoutDirection); } public boolean setTopAndBottomOffset(int offset) { if (viewOffsetHelper != null) { return viewOffsetHelper.setTopAndBottomOffset(offset); } else { tempTopBottomOffset = offset; } return false; } public boolean setLeftAndRightOffset(int offset) { if (viewOffsetHelper != null) { return viewOffsetHelper.setLeftAndRightOffset(offset); } else { tempLeftRightOffset = offset; } return false; } public int getTopAndBottomOffset() { return viewOffsetHelper != null ? viewOffsetHelper.getTopAndBottomOffset() : 0; } public int getLeftAndRightOffset() { return viewOffsetHelper != null ? viewOffsetHelper.getLeftAndRightOffset() : 0; } public void setVerticalOffsetEnabled(boolean verticalOffsetEnabled) { if (viewOffsetHelper != null) { viewOffsetHelper.setVerticalOffsetEnabled(verticalOffsetEnabled); } } public int getLayoutTop() { if (viewOffsetHelper != null) { return viewOffsetHelper.getLayoutTop(); } return 0; } public int getLayoutLeft() { if (viewOffsetHelper != null) { return viewOffsetHelper.getLayoutLeft(); } return 0; } public boolean isVerticalOffsetEnabled() { return viewOffsetHelper != null && viewOffsetHelper.isVerticalOffsetEnabled(); } public void setHorizontalOffsetEnabled(boolean horizontalOffsetEnabled) { if (viewOffsetHelper != null) { viewOffsetHelper.setHorizontalOffsetEnabled(horizontalOffsetEnabled); } } public boolean isHorizontalOffsetEnabled() { return viewOffsetHelper != null && viewOffsetHelper.isHorizontalOffsetEnabled(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/qqface/IQMUIQQFaceManager.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.qqface; import android.graphics.drawable.Drawable; /** * QMUIQQFace资源管理接口,其实现参考QMUIDemo * 1. 不想将所有emoji表情资源全都打包到qmui中 * 2. 使用者可以高度自定义表情资源 * * @author cginechen * @date 2016-12-21 */ public interface IQMUIQQFaceManager { /** * 判断是否是SoftBank编码表情,用于缩小解析范围,返回true调用{@link #getSoftbankEmojiResource}进行解析 * SoftBank虽然是Unicode编码,但是位于BMP,所以可以直接通过char来做判断 *

* Android很多输入法都自带一套SoftBank表情,但不同输入法的表情并不统一。 */ boolean maybeSoftBankEmoji(char c); /** * 获取SoftBank编码表情,如果没有则返回0 */ int getSoftbankEmojiResource(char c); /** * 判断Unicode编码字符是否是表情,用于缩小解析范围,返回true调用{@link #getEmojiResource}进行解析 */ boolean maybeEmoji(int codePoint); /** * 获取Unicode编码表情,如果没有则返回0 */ int getEmojiResource(int codePoint); /** * 获取双字符编码表情, 如果没有则返回0 */ int getDoubleUnicodeEmoji(int currentCodePoint, int nextCodePoint); /** * 将字符串解析例为表情,如果没有则返回0 */ int getQQfaceResource(CharSequence text); /** * 寻找特殊bounds的Drawable, 字符串请以[开头和以]结尾 */ Drawable getSpecialBoundsDrawable(CharSequence text); /** * 获取特殊bounds的Drawable的最高height, 这将决定行高 * fixme: 目前会影响所有行,要改为只影响含有特殊Drawable的行 */ int getSpecialDrawableMaxHeight(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUINoQQFaceManager.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.qqface; import android.graphics.drawable.Drawable; public class QMUINoQQFaceManager implements IQMUIQQFaceManager{ @Override public boolean maybeSoftBankEmoji(char c) { return false; } @Override public int getSoftbankEmojiResource(char c) { return 0; } @Override public boolean maybeEmoji(int codePoint) { return false; } @Override public int getEmojiResource(int codePoint) { return 0; } @Override public int getDoubleUnicodeEmoji(int currentCodePoint, int nextCodePoint) { return 0; } @Override public int getQQfaceResource(CharSequence text) { return 0; } @Override public Drawable getSpecialBoundsDrawable(CharSequence text) { return null; } @Override public int getSpecialDrawableMaxHeight() { return 0; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceCompiler.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.qqface; import android.graphics.drawable.Drawable; import android.text.Spannable; import android.util.LruCache; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import com.qmuiteam.qmui.span.QMUITouchableSpan; import com.qmuiteam.qmui.util.QMUILangHelper; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * {@link QMUIQQFaceView} 的内容解析器,将文本内容解析成 {@link QMUIQQFaceView} 想要的数据格式。 * * @author cginechen * @date 2016-12-21 */ public class QMUIQQFaceCompiler { private static final int SPAN_COLUMN = 2; private static final Map sInstanceMap = new HashMap<>(4); private static IQMUIQQFaceManager sDefaultQQFaceManager = new QMUINoQQFaceManager(); public static void setDefaultQQFaceManager(@NonNull IQMUIQQFaceManager defaultQQFaceManager) { sDefaultQQFaceManager = defaultQQFaceManager; } private LruCache mCache; private IQMUIQQFaceManager mQQFaceManager; @MainThread public static QMUIQQFaceCompiler getDefaultInstance(){ return getInstance(sDefaultQQFaceManager); } @MainThread public static QMUIQQFaceCompiler getInstance(IQMUIQQFaceManager manager) { QMUIQQFaceCompiler instance = sInstanceMap.get(manager); if (instance != null) { return instance; } instance = new QMUIQQFaceCompiler(manager); sInstanceMap.put(manager, instance); return instance; } private QMUIQQFaceCompiler(IQMUIQQFaceManager manager) { mCache = new LruCache<>(30); mQQFaceManager = manager; } public int getSpecialBoundsMaxHeight() { return mQQFaceManager.getSpecialDrawableMaxHeight(); } public ElementList compile(CharSequence text) { if (QMUILangHelper.isNullOrEmpty(text)) { return null; } return compile(text, 0, text.length()); } public ElementList compile(CharSequence text, int start, int end) { return compile(text, start, end, false); } private ElementList compile(CharSequence text, int start, int end, boolean inSpan) { if (QMUILangHelper.isNullOrEmpty(text)) { return null; } if (start < 0 || start >= text.length()) { throw new IllegalArgumentException("start must >= 0 and < text.length"); } if (end <= start) { throw new IllegalArgumentException("end must > start"); } int size = text.length(); if (end > size) { end = size; } boolean hasClickableSpans = false; QMUITouchableSpan[] spans = null; int[] spanInfo = null; if (!inSpan && (text instanceof Spannable)) { final Spannable spannable = (Spannable) text; spans = ((Spannable) text).getSpans( 0, text.length() - 1, QMUITouchableSpan.class); Arrays.sort(spans, new Comparator() { @Override public int compare(QMUITouchableSpan o1, QMUITouchableSpan o2) { int start1 = spannable.getSpanStart(o1); int start2 = spannable.getSpanStart(o2); if (start1 > start2) { return 1; } else if (start1 == start2) { return 0; } return -1; } }); hasClickableSpans = spans.length > 0; if (hasClickableSpans) { spanInfo = new int[spans.length * SPAN_COLUMN]; for (int i = 0; i < spans.length; i++) { spanInfo[i * SPAN_COLUMN] = spannable.getSpanStart(spans[i]); spanInfo[i * SPAN_COLUMN + 1] = spannable.getSpanEnd(spans[i]); } } } ElementList elementList = mCache.get(text); if (!hasClickableSpans && elementList != null && start == elementList.getStart() && end == elementList.getEnd()) { return elementList; } elementList = realCompile(text, start, end, spans, spanInfo); if(!hasClickableSpans && !inSpan){ mCache.put(text, elementList); } return elementList; } public void setCache(LruCache cache) { mCache = cache; } @SuppressWarnings("ConstantConditions") private ElementList realCompile(CharSequence text, int start, int end, QMUITouchableSpan[] spans, int[] spanInfo) { int size = text.length(); int nearSpanIndex = -1; int nearSpanStart = Integer.MAX_VALUE; int nearSpanEnd = nearSpanStart; if (spans != null && spans.length > 0) { nearSpanIndex = 0; nearSpanStart = spanInfo[0]; nearSpanEnd = spanInfo[1]; } ElementList elementList = new ElementList(start, end); if (start > 0) { elementList.add(Element.createTextElement(text.subSequence(0, start))); } int index = start, last = start; boolean inParentheses = false; while (index < end) { // 优先处理Span的情况 if (index == nearSpanStart) { if (index - last > 0) { if (inParentheses) { inParentheses = false; last--; } elementList.add(Element.createTextElement(text.subSequence(last, index))); } elementList.add(Element.createTouchSpanElement( text.subSequence(nearSpanStart, nearSpanEnd), spans[nearSpanIndex], this)); index = last = nearSpanEnd; nearSpanIndex++; if (nearSpanIndex >= spans.length) { nearSpanStart = nearSpanEnd = Integer.MAX_VALUE; } else { nearSpanStart = spanInfo[nearSpanIndex * SPAN_COLUMN]; nearSpanEnd = spanInfo[nearSpanIndex * SPAN_COLUMN + 1]; } continue; } char c = text.charAt(index); if (c == '[') { if (index - last > 0) { elementList.add(Element.createTextElement(text.subSequence(last, index))); } inParentheses = true; last = index++; continue; } else if (c == ']' && inParentheses) { inParentheses = false; index++; if (index - last > 0) { String label = text.subSequence(last, index).toString(); Drawable specialDrawable = mQQFaceManager.getSpecialBoundsDrawable(label); if (specialDrawable != null) { elementList.add(Element.createSpeaicalBoundsDrawableElement(specialDrawable)); last = index; } else { int res = mQQFaceManager.getQQfaceResource(label); if (res != 0) { elementList.add(Element.createDrawableElement(res)); last = index; } } } continue; } else if (c == '\n') { if (inParentheses) { inParentheses = false; } if (index - last > 0) { elementList.add(Element.createTextElement(text.subSequence(last, index))); } elementList.add(Element.createNextLineElement()); last = ++index; continue; } if (inParentheses) { if (index - last > 8) { inParentheses = false; } else { index++; continue; } } int skip = 0; int icon = 0; if (mQQFaceManager.maybeSoftBankEmoji(c)) { icon = mQQFaceManager.getSoftbankEmojiResource(c); skip = icon == 0 ? 0 : 1; } if (icon == 0) { int unicode = Character.codePointAt(text, index); skip = Character.charCount(unicode); if (mQQFaceManager.maybeEmoji(unicode)) { icon = mQQFaceManager.getEmojiResource(unicode); } if (icon == 0 && start + skip < end) { int nextUnicode = Character.codePointAt(text, start + skip); icon = mQQFaceManager.getDoubleUnicodeEmoji(unicode, nextUnicode); if (icon != 0) { skip += Character.charCount(nextUnicode); } } } if (icon != 0) { if (last != index) { elementList.add(Element.createTextElement(text.subSequence(last, index))); } elementList.add(Element.createDrawableElement(icon)); index += skip; last = index; } else { index++; } } if (last < end) { elementList.add(Element.createTextElement(text.subSequence(last, size))); } return elementList; } public enum ElementType { TEXT, DRAWABLE, SPECIAL_BOUNDS_DRAWABLE, SPAN, NEXTLINE } public static class Element { private ElementType mType; private CharSequence mText; private int mDrawableRes; private Drawable mSpecialBoundsDrawable; private ElementList mChildList; // for span private QMUITouchableSpan mTouchableSpan; public ElementType getType() { return mType; } public CharSequence getText() { return mText; } public int getDrawableRes() { return mDrawableRes; } public ElementList getChildList() { return mChildList; } public QMUITouchableSpan getTouchableSpan() { return mTouchableSpan; } public Drawable getSpecialBoundsDrawable() { return mSpecialBoundsDrawable; } public static Element createTextElement(CharSequence text) { Element element = new Element(); element.mType = ElementType.TEXT; element.mText = text; return element; } public static Element createDrawableElement(int drawableRes) { Element element = new Element(); element.mType = ElementType.DRAWABLE; element.mDrawableRes = drawableRes; return element; } public static Element createSpeaicalBoundsDrawableElement(Drawable specialBoundsDrawable) { Element element = new Element(); element.mType = ElementType.SPECIAL_BOUNDS_DRAWABLE; element.mSpecialBoundsDrawable = specialBoundsDrawable; return element; } public static Element createTouchSpanElement(CharSequence text, QMUITouchableSpan touchableSpan, QMUIQQFaceCompiler compiler) { Element element = new Element(); element.mType = ElementType.SPAN; element.mChildList = compiler.compile(text, 0, text.length(), true); element.mTouchableSpan = touchableSpan; return element; } public static Element createNextLineElement() { Element element = new Element(); element.mType = ElementType.NEXTLINE; return element; } } public static class ElementList { private int mStart; private int mEnd; private int mQQFaceCount = 0; private int mNewLineCount = 0; private List mElements; public ElementList(int start, int end) { mStart = start; mEnd = end; mElements = new ArrayList<>(); } public int getStart() { return mStart; } public int getEnd() { return mEnd; } public int getNewLineCount() { return mNewLineCount; } public int getQQFaceCount() { return mQQFaceCount; } public void add(Element element) { if (element.getType() == ElementType.DRAWABLE) { mQQFaceCount++; } else if (element.getType() == ElementType.NEXTLINE) { mNewLineCount++; } else if (element.getType() == ElementType.SPAN) { ElementList childList = element.getChildList(); if (childList != null) { mQQFaceCount += element.getChildList().getQQFaceCount(); mNewLineCount += element.getChildList().getNewLineCount(); } } mElements.add(element); } public List getElements() { return mElements; } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.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.qqface; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.link.ITouchableSpan; import com.qmuiteam.qmui.span.QMUITouchableSpan; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.List; import static android.view.View.MeasureSpec.AT_MOST; /** * 表情控件 *

*

* * @author cginechen * @date 2016-12-21 */ public class QMUIQQFaceView extends View { private static final String TAG = "QMUIQQFaceView"; private CharSequence mOriginText; private QMUIQQFaceCompiler.ElementList mElementList; private QMUIQQFaceCompiler mCompiler; private boolean mOpenQQFace = true; private TextPaint mPaint; private Paint mDecorationPaint; private int mTextSize; private ColorStateList mTextColor; private int mLineSpace = -1; private int mFontHeight; private int mQQFaceSize = 0; private int mFirstBaseLine; private int mMaxLine = Integer.MAX_VALUE; private boolean mIsSingleLine = false; private int mLines = 0; private HashMap mSpanInfos = new HashMap<>(); private boolean mIsTouchDownInMoreText = false; private Rect mMoreHitRect = new Rect(); private static final String mEllipsizeText = "..."; private String mMoreActionText; private ColorStateList mMoreActionColor; private ColorStateList mMoreActionBgColor; private int mMoreActionTextLength = 0; private int mEllipsizeTextLength = 0; private TextUtils.TruncateAt mEllipsize = TextUtils.TruncateAt.END; private boolean mIsNeedEllipsize = false; private int mNeedDrawLine = 0; private int mParagraphShowCount = 0; private int mQQFaceSizeAddon = 0; // 可以让QQ表情高度比字体高度小一点或大一点 private QQFaceViewListener mListener; private int mMaxWidth = Integer.MAX_VALUE; private PressCancelAction mPendingPressCancelAction = null; private boolean mJumpHandleMeasureAndDraw = false; private boolean mIncludePad = true; private Typeface mTypeface = null; private int mParagraphSpace = 0; // 段间距 private int mSpecialDrawablePadding = 0; private int mGravity = Gravity.NO_GRAVITY; private final int[] mPressedState = new int[]{ android.R.attr.state_pressed, android.R.attr.state_enabled }; private boolean mIsNeedUnderlineForMoreText = false; private ColorStateList mLinkUnderLineColor; private int mLinkUnderLineHeight = 1; public QMUIQQFaceView(Context context) { this(context, null); } public QMUIQQFaceView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUIQQFaceStyle); } public QMUIQQFaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.QMUIQQFaceView, defStyleAttr, 0); mQQFaceSizeAddon = -QMUIDisplayHelper.dp2px(context, 2); // 默认表情小一点好看 mTextSize = array.getDimensionPixelSize(R.styleable.QMUIQQFaceView_android_textSize, QMUIDisplayHelper.dp2px(context, 14)); mTextColor = array.getColorStateList(R.styleable.QMUIQQFaceView_android_textColor); mIsSingleLine = array.getBoolean(R.styleable.QMUIQQFaceView_android_singleLine, false); mMaxLine = array.getInt(R.styleable.QMUIQQFaceView_android_maxLines, mMaxLine); int lineSpace = array.getDimensionPixelOffset(R. styleable.QMUIQQFaceView_android_lineSpacingExtra, 0); setLineSpace(lineSpace); int ellipsize = -1; ellipsize = array.getInt(R.styleable.QMUIQQFaceView_android_ellipsize, ellipsize); switch (ellipsize) { case 1: mEllipsize = TextUtils.TruncateAt.START; break; case 2: mEllipsize = TextUtils.TruncateAt.MIDDLE; break; case 3: mEllipsize = TextUtils.TruncateAt.END; break; default: mEllipsize = null; break; } mMaxWidth = array.getDimensionPixelSize(R.styleable.QMUIQQFaceView_android_maxWidth, mMaxWidth); mSpecialDrawablePadding = array.getDimensionPixelSize(R.styleable.QMUIQQFaceView_qmui_special_drawable_padding, 0); final String text = array.getString(R.styleable.QMUIQQFaceView_android_text); if (!QMUILangHelper.isNullOrEmpty(text)) { mOriginText = text; } mMoreActionText = array.getString(R.styleable.QMUIQQFaceView_qmui_more_action_text); mMoreActionColor = array.getColorStateList(R.styleable.QMUIQQFaceView_qmui_more_action_color); mMoreActionBgColor = array.getColorStateList(R.styleable.QMUIQQFaceView_qmui_more_action_bg_color); array.recycle(); mPaint = new TextPaint(); mPaint.setAntiAlias(true); mPaint.setTextSize(mTextSize); mEllipsizeTextLength = (int) Math.ceil(mPaint.measureText(mEllipsizeText)); measureMoreActionTextLength(); mDecorationPaint = new Paint(); mDecorationPaint.setAntiAlias(true); mDecorationPaint.setStyle(Paint.Style.FILL); setCompiler(QMUIQQFaceCompiler.getDefaultInstance()); } public void setOpenQQFace(boolean openQQFace) { mOpenQQFace = openQQFace; } public void setGravity(int gravity) { mGravity = gravity; } public int getGravity() { return mGravity; } public void setMaxWidth(int maxWidth) { if (mMaxWidth != maxWidth) { mMaxWidth = maxWidth; requestLayout(); } } public int getMaxWidth() { return mMaxWidth; } SpanInfo mTouchSpanInfo = null; @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); if (mSpanInfos.isEmpty() && mMoreHitRect.isEmpty()) { return super.onTouchEvent(event); } final int action = event.getAction(); if (action != MotionEvent.ACTION_DOWN && (!mIsTouchDownInMoreText && mTouchSpanInfo == null)) { return super.onTouchEvent(event); } // touch事件前先消耗掉还存在的mPendingPressCancelAction if (mPendingPressCancelAction != null) { mPendingPressCancelAction.run(); mPendingPressCancelAction = null; } switch (action) { case MotionEvent.ACTION_DOWN: mTouchSpanInfo = null; mIsTouchDownInMoreText = false; if (mMoreHitRect.contains(x, y)) { mIsTouchDownInMoreText = true; invalidate(mMoreHitRect); } else { for (SpanInfo spanInfo : mSpanInfos.values()) { if (spanInfo.onTouch(x, y)) { mTouchSpanInfo = spanInfo; break; } } } if (mTouchSpanInfo != null) { mTouchSpanInfo.setPressed(true); mTouchSpanInfo.invalidateSpan(); } else if (!mIsTouchDownInMoreText) { return super.onTouchEvent(event); } break; case MotionEvent.ACTION_CANCEL: mPendingPressCancelAction = null; if (mTouchSpanInfo != null) { mTouchSpanInfo.setPressed(false); mTouchSpanInfo.invalidateSpan(); } else if (mIsTouchDownInMoreText) { mIsTouchDownInMoreText = false; invalidate(mMoreHitRect); } break; case MotionEvent.ACTION_MOVE: if (mTouchSpanInfo != null && !mTouchSpanInfo.onTouch(x, y)) { mTouchSpanInfo.setPressed(false); mTouchSpanInfo.invalidateSpan(); mTouchSpanInfo = null; } else if (mIsTouchDownInMoreText && !mMoreHitRect.contains(x, y)) { mIsTouchDownInMoreText = false; invalidate(mMoreHitRect); } break; case MotionEvent.ACTION_UP: if (mTouchSpanInfo != null) { mTouchSpanInfo.onClick(); mPendingPressCancelAction = new PressCancelAction(mTouchSpanInfo); postDelayed(new Runnable() { @Override public void run() { if (mPendingPressCancelAction != null) { mPendingPressCancelAction.run(); } } }, 100); } else if (mIsTouchDownInMoreText) { if (mListener != null) { mListener.onMoreTextClick(); } else if (isClickable()) { performClick(); } mIsTouchDownInMoreText = false; invalidate(mMoreHitRect); } break; } return true; } public void setCompiler(QMUIQQFaceCompiler compiler) { if (mCompiler != compiler) { mCompiler = compiler; setText(mOriginText, false); } } public void setTypeface(Typeface typeface) { if (mTypeface != typeface) { mTypeface = typeface; needReCalculateFontHeight = true; mPaint.setTypeface(typeface); requestLayout(); invalidate(); } } public void setTypeface(Typeface tf, int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); } else { tf = Typeface.create(tf, style); } setTypeface(tf); // now compute what (if any) algorithmic styling is needed int typefaceStyle = tf != null ? tf.getStyle() : 0; int need = style & ~typefaceStyle; mPaint.setFakeBoldText((need & Typeface.BOLD) != 0); mPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); } else { mPaint.setFakeBoldText(false); mPaint.setTextSkewX(0); setTypeface(tf); } } /** * @param paragraphSpace only support for NO Ellipse or Ellipse End */ public void setParagraphSpace(int paragraphSpace) { if (mParagraphSpace != paragraphSpace) { mParagraphSpace = paragraphSpace; requestLayout(); invalidate(); } } public void setMoreActionText(String moreActionText) { if (mMoreActionText == null || !mMoreActionText.equals(moreActionText)) { mMoreActionText = moreActionText; measureMoreActionTextLength(); requestLayout(); invalidate(); } } public void setLinkUnderLineColor(int linkUnderLineColor) { setLinkUnderLineColor(ColorStateList.valueOf(linkUnderLineColor)); } public void setLinkUnderLineColor(ColorStateList linkUnderLineColor) { if (mLinkUnderLineColor != linkUnderLineColor) { mLinkUnderLineColor = linkUnderLineColor; invalidate(); } } public void setLinkUnderLineHeight(int linkUnderLineHeight) { if (mLinkUnderLineHeight != linkUnderLineHeight) { mLinkUnderLineHeight = linkUnderLineHeight; invalidate(); } } public void setNeedUnderlineForMoreText(boolean needUnderlineForMoreText) { if (mIsNeedUnderlineForMoreText != needUnderlineForMoreText) { mIsNeedUnderlineForMoreText = needUnderlineForMoreText; invalidate(); } } public void setMoreActionColor(int color) { setMoreActionColor(ColorStateList.valueOf(color)); } public void setMoreActionColor(ColorStateList color) { if (mMoreActionColor != color) { mMoreActionColor = color; invalidate(); } } public void setMoreActionBgColor(int color) { setMoreActionBgColor(ColorStateList.valueOf(color)); } public void setMoreActionBgColor(ColorStateList color) { if (mMoreActionBgColor != color) { mMoreActionBgColor = color; invalidate(); } } private void measureMoreActionTextLength() { if (QMUILangHelper.isNullOrEmpty(mMoreActionText)) { mMoreActionTextLength = 0; } else { mMoreActionTextLength = (int) Math.ceil(mPaint.measureText(mMoreActionText)); } } public void setSpecialDrawablePadding(int specialDrawablePadding) { if (mSpecialDrawablePadding != specialDrawablePadding) { mSpecialDrawablePadding = specialDrawablePadding; requestLayout(); invalidate(); } } public void setIncludeFontPadding(boolean includePad) { if (mIncludePad != includePad) { needReCalculateFontHeight = true; mIncludePad = includePad; requestLayout(); invalidate(); } } public void setQQFaceSizeAddon(int QQFaceSizeAddon) { if (mQQFaceSizeAddon != QQFaceSizeAddon) { mQQFaceSizeAddon = QQFaceSizeAddon; mNeedReCalculateLines = true; requestLayout(); invalidate(); } } public void setLineSpace(int lineSpace) { if (mLineSpace != lineSpace) { mLineSpace = lineSpace; requestLayout(); invalidate(); } } public void setEllipsize(TextUtils.TruncateAt where) { if (mEllipsize != where) { mEllipsize = where; requestLayout(); invalidate(); } } public void setMaxLine(int maxLine) { if (mMaxLine != maxLine) { mMaxLine = maxLine; requestLayout(); invalidate(); } } public int getMaxLine() { return mMaxLine; } public int getLineCount() { return mLines; } public boolean isNeedEllipsize() { return mIsNeedEllipsize; } public void setSingleLine(boolean singleLine) { if (mIsSingleLine != singleLine) { mIsSingleLine = singleLine; requestLayout(); invalidate(); } } public void setTextColor(@ColorInt int textColor) { setTextColor(ColorStateList.valueOf(textColor)); } public void setTextColor(ColorStateList textColor) { if (mTextColor != textColor) { mTextColor = textColor; invalidate(); } } public TextPaint getPaint() { return mPaint; } public void setTextSize(int textSize) { if (mTextSize != textSize) { mTextSize = textSize; mPaint.setTextSize(mTextSize); needReCalculateFontHeight = true; mNeedReCalculateLines = true; mEllipsizeTextLength = (int) Math.ceil(mPaint.measureText(mEllipsizeText)); measureMoreActionTextLength(); requestLayout(); invalidate(); } } public int getTextSize() { return mTextSize; } public CharSequence getText() { return mOriginText; } /** * make sense only work after draw * * @return */ public Rect getMoreHitRect() { return mMoreHitRect; } public void setText(CharSequence charSequence) { setText(charSequence, true); } private void setText(CharSequence charSequence, boolean compareOldText) { if (compareOldText && QMUILangHelper.objectEquals(charSequence, mOriginText)) { return; } mOriginText = charSequence; setContentDescription(charSequence); if (mOpenQQFace && mCompiler == null) { throw new RuntimeException("mCompiler == null"); } mSpanInfos.clear(); if (QMUILangHelper.isNullOrEmpty(mOriginText)) { mElementList = null; requestLayout(); invalidate(); return; } if (mOpenQQFace && mCompiler != null) { mElementList = mCompiler.compile(mOriginText); List elements = mElementList.getElements(); if (elements != null) { for (int i = 0; i < elements.size(); i++) { QMUIQQFaceCompiler.Element element = elements.get(i); if (element.getType() == QMUIQQFaceCompiler.ElementType.SPAN) { mSpanInfos.put(element, new SpanInfo(element.getTouchableSpan())); } } } } else { mElementList = new QMUIQQFaceCompiler.ElementList(0, mOriginText.length()); String[] strings = mOriginText.toString().split("\\n"); for (int i = 0; i < strings.length; i++) { mElementList.add(QMUIQQFaceCompiler.Element.createTextElement(strings[i])); if (i != strings.length - 1) { mElementList.add(QMUIQQFaceCompiler.Element.createNextLineElement()); } } } mNeedReCalculateLines = true; if (getLayoutParams() == null) { return; } if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT || getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) { requestLayout(); invalidate(); return; } int paddingHor = getPaddingLeft() + getPaddingRight(); int paddingVer = getPaddingBottom() + getPaddingTop(); if (getWidth() > paddingHor && getHeight() > paddingVer) { mLines = 0; calculateLinesAndContentWidth(getWidth()); int oldDrawLine = mNeedDrawLine; int maxLine = Math.min((getHeight() - paddingVer + mLineSpace) / (mFontHeight + mLineSpace), mMaxLine); calculateNeedDrawLine(maxLine); // 优化: 如果高度固定或者绘制的行数相同,则不进行requestLayout if (oldDrawLine == mNeedDrawLine) { invalidate(); } else { requestLayout(); invalidate(); } } } private boolean needReCalculateFontHeight = true; protected int calculateFontHeight() { if (needReCalculateFontHeight) { Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt(); if (fontMetricsInt == null) { mFontHeight = mQQFaceSize = 0; } else { needReCalculateFontHeight = false; int top = getFontHeightCalTop(fontMetricsInt, mIncludePad); int bottom = getFontHeightCalBottom(fontMetricsInt, mIncludePad); int fontHeight = bottom - top; mQQFaceSize = fontHeight + mQQFaceSizeAddon; int specialMaxDrawableHeight = mCompiler.getSpecialBoundsMaxHeight(); int drawableSize = Math.max(mQQFaceSize, specialMaxDrawableHeight); if (fontHeight >= drawableSize) { mFontHeight = fontHeight; mFirstBaseLine = -top; } else { mFontHeight = drawableSize; mFirstBaseLine = -top + (drawableSize - fontHeight) / 2; } } } return mFontHeight; } public int getFontHeight() { return mFontHeight; } public int getLineSpace() { return mLineSpace; } protected int getFontHeightCalTop(Paint.FontMetricsInt fontMetricsInt, boolean includePad) { return includePad ? fontMetricsInt.top : fontMetricsInt.ascent; } protected int getFontHeightCalBottom(Paint.FontMetricsInt fontMetricsInt, boolean includePad) { return includePad ? fontMetricsInt.bottom : fontMetricsInt.descent; } @Override public void setPadding(int left, int top, int right, int bottom) { if (getPaddingLeft() != left || getPaddingRight() != right) { mNeedReCalculateLines = true; } super.setPadding(left, top, right, bottom); } private int mCurrentCalWidth = 0; private int mCurrentCalLine = 0; private int mContentCalMaxWidth = 0; private boolean mNeedReCalculateLines = false; // 缓存,避免onMeasure重复计算 private int mLastCalLimitWidth = 0; private int mLastCalContentWidth = 0; private int mLastCalLines = 0; protected int calculateLinesAndContentWidth(int limitWidth) { if (limitWidth <= (getPaddingRight() + getPaddingLeft()) || isElementEmpty()) { mLines = 0; mParagraphShowCount = 0; mLastCalLines = 0; mLastCalContentWidth = 0; return mLastCalContentWidth; } if (!mNeedReCalculateLines && mLastCalLimitWidth == limitWidth) { mLines = mLastCalLines; return mLastCalContentWidth; } mLastCalLimitWidth = limitWidth; List elements = mElementList.getElements(); mCurrentCalLine = 1; mCurrentCalWidth = getPaddingLeft(); calculateLinesInner(elements, limitWidth); if (mCurrentCalLine != mLines) { if (mListener != null) { mListener.onCalculateLinesChange(mCurrentCalLine); } mLines = mCurrentCalLine; } if (mLines == 1) { mLastCalContentWidth = mCurrentCalWidth + getPaddingRight(); } else { mLastCalContentWidth = limitWidth; } mLastCalLines = mLines; return mLastCalContentWidth; } private void calculateNeedDrawLine(int maxline) { mNeedDrawLine = mLines; if (mIsSingleLine) { mNeedDrawLine = Math.min(1, mLines); } else if (maxline < mLines) { mNeedDrawLine = maxline; } mIsNeedEllipsize = mLines > mNeedDrawLine; } private void calculateLinesInner(List elements, int limitWidth) { QMUIQQFaceCompiler.Element element; int widthStart = getPaddingLeft(), widthEnd = limitWidth - getPaddingRight(); for (int i = 0; i < elements.size(); i++) { if (mJumpHandleMeasureAndDraw) { break; } if (mCurrentCalLine > mMaxLine && mEllipsize == TextUtils.TruncateAt.END) { break; } element = elements.get(i); if (element.getType() == QMUIQQFaceCompiler.ElementType.DRAWABLE) { if (mCurrentCalWidth + mQQFaceSize > widthEnd) { gotoCalNextLine(widthStart); } mCurrentCalWidth += mQQFaceSize; if (widthEnd - widthStart < mQQFaceSize) { // 一个表情的宽度都容不下 mJumpHandleMeasureAndDraw = true; } } else if (element.getType() == QMUIQQFaceCompiler.ElementType.TEXT) { CharSequence text = element.getText(); measureText(text, widthStart, widthEnd); } else if (element.getType() == QMUIQQFaceCompiler.ElementType.SPAN) { QMUIQQFaceCompiler.ElementList spanElementList = element.getChildList(); ITouchableSpan span = element.getTouchableSpan(); if (spanElementList != null && spanElementList.getElements().size() > 0) { if (span == null) { calculateLinesInner(spanElementList.getElements(), limitWidth); continue; } calculateLinesInner(spanElementList.getElements(), limitWidth); } } else if (element.getType() == QMUIQQFaceCompiler.ElementType.NEXTLINE) { gotoCalNextLine(widthStart, true); } else if (element.getType() == QMUIQQFaceCompiler.ElementType.SPECIAL_BOUNDS_DRAWABLE) { Drawable drawable = element.getSpecialBoundsDrawable(); int width = drawable.getIntrinsicWidth(); if (i == 0 || i == elements.size() - 1) { width += mSpecialDrawablePadding; } else { width += mSpecialDrawablePadding * 2; } if (mCurrentCalWidth + width > widthEnd) { gotoCalNextLine(widthStart); mCurrentCalWidth += width; } else if (mCurrentCalWidth + width == widthEnd) { gotoCalNextLine(widthStart); } else { mCurrentCalWidth += width; } if (widthEnd - widthStart < width) { // 一个表情的宽度都容不下 mJumpHandleMeasureAndDraw = true; } } } } private boolean isElementEmpty() { return mElementList == null || mElementList.getElements() == null || mElementList.getElements().isEmpty(); } private void setContentCalMaxWidth(int width) { mContentCalMaxWidth = Math.max(width, mContentCalMaxWidth); } private void gotoCalNextLine(int widthStart) { gotoCalNextLine(widthStart, false); } private void gotoCalNextLine(int widthStart, boolean nextParagraph) { mCurrentCalLine++; setContentCalMaxWidth(mCurrentCalWidth); mCurrentCalWidth = widthStart; if (nextParagraph) { if (mEllipsize == null) { mParagraphShowCount++; } else if (mEllipsize == TextUtils.TruncateAt.END) { if (mCurrentCalLine <= mMaxLine) { mParagraphShowCount++; } } } } private void measureText(CharSequence text, int widthStart, int widthEnd) { float[] widths = new float[text.length()]; mPaint.getTextWidths(text.toString(), widths); int contentWidth = widthEnd - widthStart; long loop_start = System.currentTimeMillis(); for (int i = 0; i < widths.length; i++) { if (contentWidth < widths[i]) { // mCurrentCalWidth已经是最小值,但又一个字都容纳不下,只能说明widthEnd太小,可能还在测量中 mJumpHandleMeasureAndDraw = true; return; } if (System.currentTimeMillis() - loop_start > 2000) { // 3s还没有measure完,那就忽略本次measure以及draw QMUILog.d(TAG, "measureText: text = %s, mCurrentCalWidth = %d, " + "widthStart = %d, widthEnd = %d", text, mCurrentCalWidth, widthStart, widthEnd); mJumpHandleMeasureAndDraw = true; break; } if (mCurrentCalWidth + widths[i] > widthEnd) { gotoCalNextLine(widthStart); } mCurrentCalWidth += Math.ceil(widths[i]); } } public void setListener(QQFaceViewListener listener) { mListener = listener; } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setText(getText()); info.setContentDescription(getText()); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { long start = System.currentTimeMillis(); mJumpHandleMeasureAndDraw = false; calculateFontHeight(); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); mLines = 0; mParagraphShowCount = 0; int width, height; switch (widthMode) { case AT_MOST: default: if (mOriginText == null || mOriginText.length() == 0) { width = 0; } else { width = calculateLinesAndContentWidth(Math.min(widthSize, mMaxWidth)); } break; case MeasureSpec.EXACTLY: case MeasureSpec.UNSPECIFIED: width = widthSize; calculateLinesAndContentWidth(width); } if (mJumpHandleMeasureAndDraw) { setMeasuredDimension(width, heightMode == AT_MOST ? 0 : heightSize); return; } int maxLine = mMaxLine; switch (heightMode) { case AT_MOST: // calculate line count first maxLine = (heightSize - getPaddingTop() - getPaddingBottom() + mLineSpace) / (mFontHeight + mLineSpace); maxLine = Math.min(maxLine, mMaxLine); calculateNeedDrawLine(maxLine); height = getPaddingTop() + getPaddingBottom(); if (mNeedDrawLine < 2) { height += mNeedDrawLine * mFontHeight; } else { height += (mNeedDrawLine - 1) * (mFontHeight + mLineSpace) + mFontHeight + mParagraphShowCount * mParagraphSpace; } break; case MeasureSpec.UNSPECIFIED: default: // calculate line count first calculateNeedDrawLine(mMaxLine); height = getPaddingTop() + getPaddingBottom(); if (mNeedDrawLine < 2) { height += mNeedDrawLine * mFontHeight; } else { height += (mNeedDrawLine - 1) * (mFontHeight + mLineSpace) + mFontHeight + mParagraphShowCount * mParagraphSpace; } break; case MeasureSpec.EXACTLY: height = heightSize; maxLine = (height - getPaddingTop() - getPaddingBottom() + mLineSpace) / (mFontHeight + mLineSpace); maxLine = Math.min(maxLine, mMaxLine); calculateNeedDrawLine(maxLine); break; } setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { if (mJumpHandleMeasureAndDraw || mOriginText == null || mLines == 0 || isElementEmpty()) { return; } pickTextPaintColor(); List elements = mElementList.getElements(); mCurrentDrawBaseLine = getPaddingTop() + mFirstBaseLine; mCurrentDrawLine = 1; setStartDrawUsedWidth(getPaddingLeft(), getWidth() - getPaddingLeft() - getPaddingRight()); mIsExecutedMiddleEllipsize = false; drawElements(canvas, elements, getWidth() - getPaddingLeft() - getPaddingRight()); } private void pickTextPaintColor() { if (mTextColor != null) { int defaultColor = mTextColor.getDefaultColor(); if (isPressed()) { mPaint.setColor(mTextColor.getColorForState(mPressedState, defaultColor)); } else { mPaint.setColor(defaultColor); } } } private int mCurrentDrawBaseLine; private int mCurrentDrawLine; private int mCurrentDrawUsedWidth; private boolean mIsInDrawSpan = false; private QMUITouchableSpan mCurrentDrawSpan; private void drawElements(Canvas canvas, List elements, int usefulWidth) { int startLeft = getPaddingLeft(), endWidth = usefulWidth + startLeft; if (mIsNeedEllipsize && mEllipsize == TextUtils.TruncateAt.START) { canvas.drawText(mEllipsizeText, 0, mEllipsizeText.length(), startLeft, mFirstBaseLine, mPaint); } QMUIQQFaceCompiler.Element element; for (int i = 0; i < elements.size(); i++) { element = elements.get(i); QMUIQQFaceCompiler.ElementType type = element.getType(); if (type == QMUIQQFaceCompiler.ElementType.DRAWABLE) { onDrawQQFace(canvas, element.getDrawableRes(), null, startLeft, endWidth, i == 0, i == elements.size() - 1); } else if (type == QMUIQQFaceCompiler.ElementType.SPECIAL_BOUNDS_DRAWABLE) { onDrawQQFace(canvas, 0, element.getSpecialBoundsDrawable(), startLeft, endWidth, i == 0, i == elements.size() - 1); } else if (type == QMUIQQFaceCompiler.ElementType.TEXT) { CharSequence text = element.getText(); float[] fontWidths = new float[text.length()]; mPaint.getTextWidths(text.toString(), fontWidths); onDrawText(canvas, text, fontWidths, 0, startLeft, endWidth); } else if (type == QMUIQQFaceCompiler.ElementType.SPAN) { QMUIQQFaceCompiler.ElementList spanElementList = element.getChildList(); mCurrentDrawSpan = element.getTouchableSpan(); SpanInfo spanInfo = mSpanInfos.get(element); if (spanElementList != null && !spanElementList.getElements().isEmpty()) { if (mCurrentDrawSpan == null) { drawElements(canvas, spanElementList.getElements(), usefulWidth); continue; } mIsInDrawSpan = true; if (spanInfo != null) { spanInfo.setStart(mCurrentDrawLine, mCurrentDrawUsedWidth); } @ColorInt int spanColor = mCurrentDrawSpan.isPressed() ? mCurrentDrawSpan.getPressedTextColor() : mCurrentDrawSpan.getNormalTextColor(); if (spanColor == 0) { pickTextPaintColor(); } else { mPaint.setColor(spanColor); } drawElements(canvas, spanElementList.getElements(), usefulWidth); pickTextPaintColor(); if (spanInfo != null) { spanInfo.setEnd(mCurrentDrawLine, mCurrentDrawUsedWidth); } mIsInDrawSpan = false; } } else if (type == QMUIQQFaceCompiler.ElementType.NEXTLINE) { int ellipsizeLength = mEllipsizeTextLength + mMoreActionTextLength; if (mIsNeedEllipsize && mEllipsize == TextUtils.TruncateAt.END && mCurrentDrawUsedWidth <= endWidth - ellipsizeLength && mCurrentDrawLine == mNeedDrawLine) { drawText(canvas, mEllipsizeText, 0, mEllipsizeText.length(), mEllipsizeTextLength); mCurrentDrawUsedWidth += mEllipsizeTextLength; drawMoreActionText(canvas, endWidth); return; } toNewDrawLine(startLeft, true, usefulWidth); } } } private void drawMoreActionText(Canvas canvas, int widthEnd) { if (!QMUILangHelper.isNullOrEmpty(mMoreActionText)) { ColorStateList colorStateList = mMoreActionColor == null ? mTextColor : mMoreActionColor; int bgColor = 0; int color = 0; if (colorStateList != null) { color = colorStateList.getDefaultColor(); if (mIsTouchDownInMoreText) { color = colorStateList.getColorForState(mPressedState, color); } } if (mMoreActionBgColor != null) { bgColor = mMoreActionBgColor.getDefaultColor(); if (mIsTouchDownInMoreText) { bgColor = mMoreActionBgColor.getColorForState(mPressedState, bgColor); } } int top = getPaddingTop(); if (mCurrentDrawLine > 1) { top = (mCurrentDrawLine - 1) * (mFontHeight + mLineSpace) + top; } mMoreHitRect.set(mCurrentDrawUsedWidth, top, mCurrentDrawUsedWidth + mMoreActionTextLength, top + mFontHeight); if (bgColor != 0) { mDecorationPaint.setColor(bgColor); mDecorationPaint.setStyle(Paint.Style.FILL); canvas.drawRect(mMoreHitRect, mDecorationPaint); } mPaint.setColor(color); canvas.drawText(mMoreActionText, 0, mMoreActionText.length(), mCurrentDrawUsedWidth, mCurrentDrawBaseLine, mPaint); if (mIsNeedUnderlineForMoreText && mLinkUnderLineHeight > 0) { ColorStateList underLineColors = mLinkUnderLineColor == null ? mTextColor : mLinkUnderLineColor; if (underLineColors != null) { int underLineColor = underLineColors.getDefaultColor(); if (mIsTouchDownInMoreText) { underLineColor = underLineColors.getColorForState(mPressedState, underLineColor); } mDecorationPaint.setColor(underLineColor); mDecorationPaint.setStyle(Paint.Style.STROKE); mDecorationPaint.setStrokeWidth(mLinkUnderLineHeight); canvas.drawLine(mMoreHitRect.left, mMoreHitRect.bottom, mMoreHitRect.right, mMoreHitRect.bottom, mDecorationPaint); } } pickTextPaintColor(); } } private void toNewDrawLine(int startLeft, int usefulWidth) { toNewDrawLine(startLeft, false, usefulWidth); } /** * control for paragraph space if mEllipsize == null || mEllipsize == TextUtils.TruncateAt.END */ private void toNewDrawLine(int startLeft, boolean paragraph, int usefulWidth) { int addOn = (paragraph && (mEllipsize == null || mEllipsize == TextUtils.TruncateAt.END) ? mParagraphSpace : 0) + mLineSpace; mCurrentDrawLine++; if (mIsNeedEllipsize) { if (mEllipsize == TextUtils.TruncateAt.START) { if (mCurrentDrawLine > mLines - mNeedDrawLine + 1) { mCurrentDrawBaseLine += mFontHeight + addOn; } } else if (mEllipsize == TextUtils.TruncateAt.MIDDLE) { if (!mIsExecutedMiddleEllipsize || mMiddleEllipsizeWidthRecord == -1) { mCurrentDrawBaseLine += mFontHeight + addOn; } } else { mCurrentDrawBaseLine += mFontHeight + addOn; } if (mEllipsize != null && mEllipsize != TextUtils.TruncateAt.END && mCurrentDrawBaseLine > getHeight() - getPaddingBottom()) { QMUILog.d(TAG, "draw outside the visible height, the ellipsize is inaccurate: " + "mEllipsize = %s; mCurrentDrawLine = %d; mNeedDrawLine = %d;" + "viewWidth = %d; viewHeight = %d; paddingLeft = %d; " + "paddingRight = %d; paddingTop = %d; paddingBottom = %d; text = %s", mEllipsize.name(), mCurrentDrawLine, mNeedDrawLine, getWidth(), getHeight(), getPaddingLeft(), getPaddingRight(), getPaddingTop(), getPaddingBottom(), mOriginText); } } else { mCurrentDrawBaseLine += mFontHeight + addOn; } setStartDrawUsedWidth(startLeft, usefulWidth); } private void setStartDrawUsedWidth(int startLeft, int usefulWidth) { if (mIsNeedEllipsize) { mCurrentDrawUsedWidth = startLeft; return; } if (mCurrentDrawLine == mNeedDrawLine) { if (mGravity == Gravity.CENTER) { mCurrentDrawUsedWidth = (usefulWidth - (mCurrentCalWidth - startLeft)) / 2 + startLeft; } else if (mGravity == Gravity.RIGHT) { mCurrentDrawUsedWidth = (usefulWidth - (mCurrentCalWidth - startLeft)) + startLeft; } else { mCurrentDrawUsedWidth = startLeft; } } else { mCurrentDrawUsedWidth = startLeft; } } private void onRealDrawText(Canvas canvas, CharSequence text, float[] fontWidths, int offset, int widthStart, int widthEnd) { int startPos = offset; int targetUsedWidth = mCurrentDrawUsedWidth; for (int i = offset; i < fontWidths.length; i++) { if (targetUsedWidth + fontWidths[i] > widthEnd) { drawText(canvas, text, startPos, i, widthEnd - mCurrentDrawUsedWidth); toNewDrawLine(widthStart, widthEnd - widthStart); targetUsedWidth = mCurrentDrawUsedWidth; startPos = i; } targetUsedWidth += fontWidths[i]; } if (startPos < fontWidths.length) { drawText(canvas, text, startPos, fontWidths.length, targetUsedWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetUsedWidth; } } private int getMiddleEllipsizeLine() { int ellipsizeLine; if (mNeedDrawLine % 2 == 0) { ellipsizeLine = mNeedDrawLine / 2; } else { ellipsizeLine = (mNeedDrawLine + 1) / 2; } return ellipsizeLine; } private int mMiddleEllipsizeWidthRecord = -1; private boolean mIsExecutedMiddleEllipsize = false; private void onDrawText(Canvas canvas, CharSequence text, float[] fontWidths, int offset, int widthStart, int widthEnd) { if (offset >= text.length()) { return; } if (mIsNeedEllipsize) { if (mEllipsize == TextUtils.TruncateAt.START) { if (mCurrentDrawLine > mLines - mNeedDrawLine) { onRealDrawText(canvas, text, fontWidths, offset, widthStart, widthEnd); } else if (mCurrentDrawLine < mLines - mNeedDrawLine) { for (int i = offset; i < text.length(); i++) { if (mCurrentDrawUsedWidth + fontWidths[i] <= widthEnd) { mCurrentDrawUsedWidth += fontWidths[i]; } else { toNewDrawLine(widthStart, widthEnd - widthStart); onDrawText(canvas, text, fontWidths, i, widthStart, widthEnd); return; } } } else { int needStopWidth = mCurrentCalWidth + mEllipsizeTextLength; for (int i = offset; i < text.length(); i++) { if (mCurrentDrawUsedWidth + fontWidths[i] <= needStopWidth) { mCurrentDrawUsedWidth += fontWidths[i]; } else { int newStart = i + 1; if (mCurrentDrawUsedWidth > needStopWidth) { newStart = i; } toNewDrawLine(widthStart + mEllipsizeTextLength, widthEnd - widthStart); onDrawText(canvas, text, fontWidths, newStart, widthStart, widthEnd); return; } } } } else if (mEllipsize == TextUtils.TruncateAt.MIDDLE) { int ellipsizeLine = getMiddleEllipsizeLine(); if (mCurrentDrawLine < ellipsizeLine) { int targetDrawWidth = mCurrentDrawUsedWidth; for (int i = offset; i < fontWidths.length; i++) { if (targetDrawWidth + fontWidths[i] <= widthEnd) { targetDrawWidth += fontWidths[i]; } else { drawText(canvas, text, offset, i, widthEnd - mCurrentDrawUsedWidth); toNewDrawLine(widthStart, widthEnd - widthStart); onDrawText(canvas, text, fontWidths, i, widthStart, widthEnd); return; } } drawText(canvas, text, offset, text.length(), targetDrawWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetDrawWidth; } else if (mCurrentDrawLine == ellipsizeLine) { if (mIsExecutedMiddleEllipsize) { handleTextAfterMiddleEllipsize(canvas, text, fontWidths, offset, ellipsizeLine, widthStart, widthEnd); } else { int needStop = (widthEnd + widthStart) / 2 - mEllipsizeTextLength / 2; int targetDrawWidth = mCurrentDrawUsedWidth; for (int i = offset; i < fontWidths.length; i++) { if (targetDrawWidth + fontWidths[i] <= needStop) { targetDrawWidth += fontWidths[i]; } else { drawText(canvas, text, offset, i, targetDrawWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetDrawWidth; drawText(canvas, mEllipsizeText, 0, mEllipsizeText.length(), mEllipsizeTextLength); mMiddleEllipsizeWidthRecord = mCurrentDrawUsedWidth + mEllipsizeTextLength; mIsExecutedMiddleEllipsize = true; handleTextAfterMiddleEllipsize(canvas, text, fontWidths, i, ellipsizeLine, widthStart, widthEnd); return; } } drawText(canvas, text, offset, text.length(), targetDrawWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetDrawWidth; } } else { handleTextAfterMiddleEllipsize(canvas, text, fontWidths, offset, ellipsizeLine, widthStart, widthEnd); } } else { if (mCurrentDrawLine < mNeedDrawLine) { int targetUsedWidth = mCurrentDrawUsedWidth; for (int i = offset; i < fontWidths.length; i++) { if (targetUsedWidth + fontWidths[i] <= widthEnd) { targetUsedWidth += fontWidths[i]; } else { drawText(canvas, text, offset, i, widthEnd - mCurrentDrawUsedWidth); toNewDrawLine(widthStart, widthEnd - widthStart); onDrawText(canvas, text, fontWidths, i, widthStart, widthEnd); return; } } drawText(canvas, text, offset, fontWidths.length, targetUsedWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetUsedWidth; } else if (mCurrentDrawLine == mNeedDrawLine) { int ellipsizeLength = mMoreActionTextLength; if (mEllipsize == TextUtils.TruncateAt.END) { ellipsizeLength += mEllipsizeTextLength; } int targetUsedWidth = mCurrentDrawUsedWidth; for (int i = offset; i < fontWidths.length; i++) { if (targetUsedWidth + fontWidths[i] <= widthEnd - ellipsizeLength) { targetUsedWidth += fontWidths[i]; } else { drawText(canvas, text, offset, i, targetUsedWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetUsedWidth; if (mEllipsize == TextUtils.TruncateAt.END) { drawText(canvas, mEllipsizeText, 0, mEllipsizeText.length(), mEllipsizeTextLength); mCurrentDrawUsedWidth += mEllipsizeTextLength; } drawMoreActionText(canvas, widthEnd); // 依然要去到下一行,使得后续不会进入这个逻辑 toNewDrawLine(widthStart, widthEnd - widthStart); return; } } drawText(canvas, text, offset, fontWidths.length, targetUsedWidth - mCurrentDrawUsedWidth); mCurrentDrawUsedWidth = targetUsedWidth; } } } else { onRealDrawText(canvas, text, fontWidths, 0, widthStart, widthEnd); } } private void handleTextAfterMiddleEllipsize(Canvas canvas, CharSequence text, float[] fontWidths, int offset, int ellipsizeLine, int widthStart, int widthEnd) { if (offset >= text.length()) { return; } if (mMiddleEllipsizeWidthRecord == -1) { onRealDrawText(canvas, text, fontWidths, offset, widthStart, widthEnd); return; } int endLines = mNeedDrawLine - ellipsizeLine; int borrowWidth = widthEnd - mCurrentCalWidth - (mMiddleEllipsizeWidthRecord - widthStart); int needStopLine = borrowWidth > 0 ? mLines - endLines - 1 : mLines - endLines; int needStopWidth = borrowWidth > 0 ? widthEnd - borrowWidth : mMiddleEllipsizeWidthRecord - (widthEnd - mCurrentCalWidth); if (mCurrentDrawLine < needStopLine) { for (int i = offset; i < fontWidths.length; i++) { if (mCurrentDrawUsedWidth + fontWidths[i] <= widthEnd) { mCurrentDrawUsedWidth += fontWidths[i]; } else { toNewDrawLine(widthStart, widthStart - widthEnd); handleTextAfterMiddleEllipsize(canvas, text, fontWidths, i, ellipsizeLine, widthStart, widthEnd); return; } } } else if (mCurrentDrawLine == needStopLine) { for (int i = offset; i < fontWidths.length; i++) { if (mCurrentDrawUsedWidth + fontWidths[i] <= needStopWidth) { mCurrentDrawUsedWidth += fontWidths[i]; } else { int newStart = i + 1; if (mCurrentDrawUsedWidth >= needStopWidth) { newStart = i; } mCurrentDrawUsedWidth = mMiddleEllipsizeWidthRecord; mMiddleEllipsizeWidthRecord = -1; mLastNeedStopLineRecord = needStopLine; onRealDrawText(canvas, text, fontWidths, newStart, widthStart, widthEnd); return; } } } else { onRealDrawText(canvas, text, fontWidths, offset, widthStart, widthEnd); } } private void drawText(Canvas canvas, CharSequence text, int start, int end, int textWidth) { if (end <= start || end > text.length() || start >= text.length()) { return; } if (mIsInDrawSpan && mCurrentDrawSpan != null) { @ColorInt int color = mCurrentDrawSpan.isPressed() ? mCurrentDrawSpan.getPressedBackgroundColor() : mCurrentDrawSpan.getNormalBackgroundColor(); if (color != Color.TRANSPARENT) { mDecorationPaint.setColor(color); mDecorationPaint.setStyle(Paint.Style.FILL); canvas.drawRect(mCurrentDrawUsedWidth, mCurrentDrawBaseLine - mFirstBaseLine, mCurrentDrawUsedWidth + textWidth, mCurrentDrawBaseLine - mFirstBaseLine + mFontHeight, mDecorationPaint); } } canvas.drawText(text, start, end, mCurrentDrawUsedWidth, mCurrentDrawBaseLine, mPaint); if (mIsInDrawSpan && mCurrentDrawSpan != null && mCurrentDrawSpan.isNeedUnderline() && mLinkUnderLineHeight > 0) { ColorStateList underLineColors = mLinkUnderLineColor == null ? mTextColor : mLinkUnderLineColor; if (underLineColors != null) { int underLineColor = underLineColors.getDefaultColor(); if (mCurrentDrawSpan.isPressed()) { underLineColor = underLineColors.getColorForState(mPressedState, underLineColor); } mDecorationPaint.setColor(underLineColor); mDecorationPaint.setStyle(Paint.Style.STROKE); mDecorationPaint.setStrokeWidth(mLinkUnderLineHeight); int bottom = mCurrentDrawBaseLine - mFirstBaseLine + mFontHeight; canvas.drawLine(mCurrentDrawUsedWidth, bottom, mCurrentDrawUsedWidth + textWidth, bottom, mDecorationPaint); } } } private void onDrawQQFace(Canvas canvas, int res, @Nullable Drawable specialDrawable, int widthStart, int widthEnd, boolean isFirst, boolean isLast) { int size = res != 0 || specialDrawable == null ? mQQFaceSize : specialDrawable.getIntrinsicWidth() + (isFirst || isLast ? mSpecialDrawablePadding : mSpecialDrawablePadding * 2); if (mIsNeedEllipsize) { if (mEllipsize == TextUtils.TruncateAt.START) { if (mCurrentDrawLine > mLines - mNeedDrawLine) { onRealDrawQQFace(canvas, res, specialDrawable, mNeedDrawLine - mLines, widthStart, widthEnd, isFirst, isLast); } else if (mCurrentDrawLine < mLines - mNeedDrawLine) { if (size + mCurrentDrawUsedWidth > widthEnd) { toNewDrawLine(widthStart, widthEnd - widthStart); onDrawQQFace(canvas, res, specialDrawable, widthStart, widthEnd, isFirst, isLast); } else { mCurrentDrawUsedWidth += size; } } else { int needStopWidth = mCurrentCalWidth + mEllipsizeTextLength; if (size + mCurrentDrawUsedWidth < needStopWidth) { mCurrentDrawUsedWidth += size; } else { toNewDrawLine(widthStart + mEllipsizeTextLength, widthEnd - widthStart); } } } else if (mEllipsize == TextUtils.TruncateAt.MIDDLE) { int ellipsizeLine = getMiddleEllipsizeLine(); if (mCurrentDrawLine < ellipsizeLine) { if (size + mCurrentDrawUsedWidth > widthEnd) { onRealDrawQQFace(canvas, res, specialDrawable, 0, widthStart, widthEnd, isFirst, isLast); } else { drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } } else if (mCurrentDrawLine == ellipsizeLine) { int needStop = getWidth() / 2 - mEllipsizeTextLength / 2; if (mIsExecutedMiddleEllipsize) { handleQQFaceAfterMiddleEllipsize(canvas, res, specialDrawable, widthStart, widthEnd, ellipsizeLine, isFirst, isLast); } else if (size + mCurrentDrawUsedWidth <= needStop) { drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } else { drawText(canvas, mEllipsizeText, 0, mEllipsizeText.length(), mEllipsizeTextLength); mMiddleEllipsizeWidthRecord = mCurrentDrawUsedWidth + mEllipsizeTextLength; mIsExecutedMiddleEllipsize = true; handleQQFaceAfterMiddleEllipsize(canvas, res, specialDrawable, widthStart, widthEnd, ellipsizeLine, isFirst, isLast); } } else { handleQQFaceAfterMiddleEllipsize(canvas, res, specialDrawable, widthStart, widthEnd, ellipsizeLine, isFirst, isLast); } } else { if (mCurrentDrawLine == mNeedDrawLine) { int ellipsizeLength = mMoreActionTextLength; if (mEllipsize == TextUtils.TruncateAt.END) { ellipsizeLength += mEllipsizeTextLength; } if (size + mCurrentDrawUsedWidth >= widthEnd - ellipsizeLength) { if (size + mCurrentDrawUsedWidth == widthEnd - ellipsizeLength) { drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } if (mEllipsize == TextUtils.TruncateAt.END) { drawText(canvas, mEllipsizeText, 0, mEllipsizeText.length(), mEllipsizeTextLength); mCurrentDrawUsedWidth += mEllipsizeTextLength; } drawMoreActionText(canvas, widthEnd); // 去新的一行,避免再次走入这一行的逻辑 toNewDrawLine(widthStart, widthEnd - widthStart); } else { drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } } else if (mCurrentDrawLine < mNeedDrawLine) { if (size + mCurrentDrawUsedWidth > widthEnd) { onRealDrawQQFace(canvas, res, specialDrawable, 0, widthStart, widthEnd, isFirst, isLast); } else { drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } } } } else { onRealDrawQQFace(canvas, res, specialDrawable, 0, widthStart, widthEnd, isFirst, isLast); } } private int mLastNeedStopLineRecord = -1; private void handleQQFaceAfterMiddleEllipsize(Canvas canvas, int res, Drawable specialDrawable, int widthStart, int widthEnd, int ellipsizeLine, boolean isFirst, boolean isLast) { int size = res != 0 ? mQQFaceSize : specialDrawable.getIntrinsicWidth() + (isFirst || isLast ? mSpecialDrawablePadding : mSpecialDrawablePadding * 2); if (mMiddleEllipsizeWidthRecord == -1) { onRealDrawQQFace(canvas, res, specialDrawable, ellipsizeLine - mLastNeedStopLineRecord, widthStart, widthEnd, isFirst, isLast); return; } int endLines = mNeedDrawLine - ellipsizeLine; int borrowWidth = widthEnd - mCurrentCalWidth - (mMiddleEllipsizeWidthRecord - widthStart); int needStopLine = borrowWidth > 0 ? mLines - endLines - 1 : mLines - endLines; int needStopWidth = borrowWidth > 0 ? widthEnd - borrowWidth : mMiddleEllipsizeWidthRecord - (widthEnd - mCurrentCalWidth); if (mCurrentDrawLine < needStopLine) { if (size + mCurrentDrawUsedWidth > widthEnd) { toNewDrawLine(widthStart, widthEnd - widthStart); onDrawQQFace(canvas, res, specialDrawable, widthStart, widthEnd, isFirst, isLast); } else { mCurrentDrawUsedWidth += size; } } else if (mCurrentDrawLine == needStopLine) { if (size + mCurrentDrawUsedWidth <= needStopWidth) { mCurrentDrawUsedWidth += size; } else { boolean drawCurrentFace = false; if (mCurrentDrawUsedWidth >= needStopWidth) { drawCurrentFace = true; } mCurrentDrawUsedWidth = mMiddleEllipsizeWidthRecord; mMiddleEllipsizeWidthRecord = -1; mLastNeedStopLineRecord = needStopLine; if (drawCurrentFace) { onDrawQQFace(canvas, res, specialDrawable, widthStart, widthEnd, isFirst, isLast); } } } else { onRealDrawQQFace(canvas, res, specialDrawable, ellipsizeLine - needStopLine, widthStart, widthEnd, isFirst, isLast); } } private void onRealDrawQQFace(Canvas canvas, int res, @Nullable Drawable specialDrawable, int adjustLine, int widthStart, int widthEnd, boolean isFirst, boolean isLast) { int size = res != 0 || specialDrawable == null ? mQQFaceSize : specialDrawable.getIntrinsicWidth() + (isFirst || isLast ? mSpecialDrawablePadding : mSpecialDrawablePadding * 2); if (mCurrentDrawUsedWidth + size > widthEnd) { toNewDrawLine(widthStart, widthEnd - widthStart); } drawQQFace(canvas, res, specialDrawable, mCurrentDrawLine + adjustLine, isFirst, isLast); mCurrentDrawUsedWidth += size; } private void drawQQFace(Canvas canvas, int res, @Nullable Drawable specialDrawable, int line, boolean isFirst, boolean isLast) { Drawable drawable = res != 0 ? ContextCompat.getDrawable(getContext(), res) : specialDrawable; int size = res != 0 || specialDrawable == null ? mQQFaceSize : specialDrawable.getIntrinsicWidth() + (isFirst || isLast ? mSpecialDrawablePadding : mSpecialDrawablePadding * 2); if (drawable == null) { return; } int drawableTop; if (res != 0) { drawableTop = (mFontHeight - mQQFaceSize) / 2; drawable.setBounds(0, drawableTop, mQQFaceSize, drawableTop + mQQFaceSize); } else { int left = isLast ? mSpecialDrawablePadding : 0; int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); if (drawableHeight > mFontHeight) { float scale = ((float) mFontHeight) / drawableHeight; drawableHeight = mFontHeight; drawableWidth = (int) (drawableWidth * scale); } drawableTop = (mFontHeight - drawableHeight) / 2; drawable.setBounds(left, drawableTop, left + drawableWidth, drawableTop + drawableHeight); } int top = getPaddingTop(); if (line > 1) { top = mCurrentDrawBaseLine - mFirstBaseLine; } canvas.save(); canvas.translate(mCurrentDrawUsedWidth, top); if (mIsInDrawSpan && mCurrentDrawSpan != null) { @ColorInt int color = mCurrentDrawSpan.isPressed() ? mCurrentDrawSpan.getPressedBackgroundColor() : mCurrentDrawSpan.getNormalBackgroundColor(); if (color != Color.TRANSPARENT) { mDecorationPaint.setColor(color); mDecorationPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0, 0, size, mFontHeight, mDecorationPaint); } } drawable.draw(canvas); if (mIsInDrawSpan && mCurrentDrawSpan != null && mCurrentDrawSpan.isNeedUnderline() && mLinkUnderLineHeight > 0) { ColorStateList underLineColors = mLinkUnderLineColor == null ? mTextColor : mLinkUnderLineColor; if (underLineColors != null) { int underLineColor = underLineColors.getDefaultColor(); if (mCurrentDrawSpan.isPressed()) { underLineColor = underLineColors.getColorForState(mPressedState, underLineColor); } mDecorationPaint.setColor(underLineColor); mDecorationPaint.setStyle(Paint.Style.STROKE); mDecorationPaint.setStrokeWidth(mLinkUnderLineHeight); canvas.drawLine(0, mFontHeight, size, mFontHeight, mDecorationPaint); } } canvas.restore(); } private class SpanInfo { public static final int NOT_SET = -1; private ITouchableSpan mTouchableSpan; private int mStartPoint = NOT_SET; private int mEndPoint = NOT_SET; private int mStartLine = NOT_SET; private int mEndLine = NOT_SET; public SpanInfo(ITouchableSpan touchableSpan) { mTouchableSpan = touchableSpan; } public void setStart(int startLine, int startPoint) { mStartLine = startLine; mStartPoint = startPoint; } public void setPressed(boolean pressed) { mTouchableSpan.setPressed(pressed); } public void setEnd(int endLine, int endPoint) { mEndLine = endLine; mEndPoint = endPoint; } public void onClick() { mTouchableSpan.onClick(QMUIQQFaceView.this); } public void invalidateSpan() { int top = getPaddingTop(); if (mStartLine > 1) { top = (mStartLine - 1) * (mFontHeight + mLineSpace) + top; } int bottom = (mEndLine - 1) * (mFontHeight + mLineSpace) + top + mFontHeight; Rect bounds = new Rect(); bounds.top = top; bounds.bottom = bottom; bounds.left = getPaddingLeft(); bounds.right = getWidth() - getPaddingRight(); if (mStartLine == mEndLine) { bounds.left = mStartPoint; bounds.right = mEndPoint; } invalidate(bounds); } @SuppressWarnings("SimplifiableIfStatement") public boolean onTouch(int x, int y) { int top = getPaddingTop(); if (mStartLine > 1) { top = (mStartLine - 1) * (mFontHeight + mLineSpace) + top; } int bottom = (mEndLine - 1) * (mFontHeight + mLineSpace) + getPaddingTop() + mFontHeight; if (y < top || y > bottom) { return false; } if (mStartLine == mEndLine) { return x >= mStartPoint && x <= mEndPoint; } int startLineBottom = top + mFontHeight; int endLineTop = bottom - mFontHeight; if (y > startLineBottom && y < endLineTop) { //noinspection SimplifiableIfStatement if (mEndLine - mStartLine == 1) { return x >= mStartPoint && x <= mEndPoint; } return true; } else if (y <= startLineBottom) { return x >= mStartPoint; } else { return x <= mEndPoint; } } } public static class PressCancelAction implements Runnable { private WeakReference mWeakReference; public PressCancelAction(SpanInfo spanInfo) { mWeakReference = new WeakReference<>(spanInfo); } @Override public void run() { SpanInfo spanInfo = mWeakReference.get(); if (spanInfo != null) { spanInfo.setPressed(false); spanInfo.invalidateSpan(); } } } public interface QQFaceViewListener { void onCalculateLinesChange(int lines); void onMoreTextClick(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/qqface/QQFace.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.qqface; /** * @author cginechen * @date 2016-12-21 */ public class QQFace { private String name; private int res; public QQFace(String name, int res) { this.name = name; this.res = res; } public String getName() { return name; } public int getRes() { return res; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVDraggableScrollBar.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.recyclerView; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.IQMUISkinHandlerDecoration; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.section.QMUIStickySectionLayout; import org.jetbrains.annotations.NotNull; public class QMUIRVDraggableScrollBar extends RecyclerView.ItemDecoration implements IQMUISkinHandlerDecoration, QMUIStickySectionLayout.DrawDecoration { private int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed}; private int[] STATE_NORMAL = new int[]{}; private static final long DEFAULT_KEE_SHOW_DURATION = 800L; private static final long DEFAULT_TRANSITION_DURATION = 100L; private static final int MIN_COUNT_FOR_PERCENT_CALCULATE = 1000; RecyclerView mRecyclerView; QMUIStickySectionLayout mStickySectionLayout; private final int mStartMargin; private final int mEndMargin; private final int mInwardOffset; private final boolean mIsVerticalScroll; private final boolean mIsLocationInOppositeSide; private boolean mIsInDragging; private Drawable mScrollBarDrawable; private boolean mEnableScrollBarFadeInOut = false; private boolean mIsDraggable = true; private Callback mCallback; private long mKeepShownTime = DEFAULT_KEE_SHOW_DURATION; private long mTransitionDuration = DEFAULT_TRANSITION_DURATION; private long mStartTransitionTime = 0; private int mBeginAlpha = -1; private int mTargetAlpha = -1; private int mCurrentAlpha = 255; private float mPercent = 0f; private int mDragInnerStart = 0; private int mScrollBarSkinRes = 0; private int mScrollBarSkinTintColorRes = 0; public QMUIRVDraggableScrollBar(int startMargin, int endMargin, int inwardOffset, boolean isVerticalScroll, boolean isLocationInOppositeSide) { mStartMargin = startMargin; mEndMargin = endMargin; mInwardOffset = inwardOffset; mIsVerticalScroll = isVerticalScroll; mIsLocationInOppositeSide = isLocationInOppositeSide; } public QMUIRVDraggableScrollBar(int startMargin, int endMargin, int inwardOffset) { this(startMargin, endMargin, inwardOffset, true, false); } private Runnable mFadeScrollBarAction = new Runnable() { @Override public void run() { mTargetAlpha = 0; mBeginAlpha = mCurrentAlpha; mStartTransitionTime = System.currentTimeMillis(); invalidate(); } }; private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { if (!mIsDraggable || mScrollBarDrawable == null || !needDrawScrollBar(rv)) { return false; } int action = e.getAction(); final int x = (int) e.getX(); final int y = (int) e.getY(); if (action == MotionEvent.ACTION_DOWN) { Rect bounds = mScrollBarDrawable.getBounds(); if (mCurrentAlpha > 0 && bounds.contains(x, y)) { startDrag(); mDragInnerStart = mIsVerticalScroll ? y - bounds.top : x - bounds.left; } } else if (action == MotionEvent.ACTION_MOVE) { if (mIsInDragging) { onDragging(rv, mScrollBarDrawable, x, y); } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mIsInDragging) { if(action == MotionEvent.ACTION_UP){ onDragging(rv, mScrollBarDrawable, x, y); } endDrag(); } } return mIsInDragging; } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { if (!mIsDraggable || mScrollBarDrawable == null || !needDrawScrollBar(rv)) { return; } int action = e.getAction(); final int x = (int) e.getX(); final int y = (int) e.getY(); if (action == MotionEvent.ACTION_DOWN) { Rect bounds = mScrollBarDrawable.getBounds(); if (mCurrentAlpha > 0 && bounds.contains(x, y)) { startDrag(); mDragInnerStart = mIsVerticalScroll ? y - bounds.top : x - bounds.left; } } else if (action == MotionEvent.ACTION_MOVE) { if (mIsInDragging) { onDragging(rv, mScrollBarDrawable, x, y); } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mIsInDragging) { if(action == MotionEvent.ACTION_UP) { onDragging(rv, mScrollBarDrawable, x, y); } endDrag(); } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept && mIsInDragging) { endDrag(); } } }; private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { private int mPrevStatus = RecyclerView.SCROLL_STATE_IDLE; @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (mEnableScrollBarFadeInOut) { if (mPrevStatus == RecyclerView.SCROLL_STATE_IDLE && newState != RecyclerView.SCROLL_STATE_IDLE) { mStartTransitionTime = System.currentTimeMillis(); mBeginAlpha = mCurrentAlpha; mTargetAlpha = 255; invalidate(); } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { recyclerView.postDelayed(mFadeScrollBarAction, mKeepShownTime); } } mPrevStatus = newState; } }; public void setCallback(Callback callback) { mCallback = callback; } private void invalidate() { if (mStickySectionLayout != null) { mStickySectionLayout.invalidate(); } else if (mRecyclerView != null) { mRecyclerView.invalidate(); } } public void setScrollBarDrawable(@Nullable Drawable scrollBarDrawable) { mScrollBarDrawable = scrollBarDrawable; if (scrollBarDrawable != null) { scrollBarDrawable.setState(mIsInDragging ? STATE_PRESSED : STATE_NORMAL); } if (mRecyclerView != null) { QMUISkinHelper.refreshRVItemDecoration(mRecyclerView, this); } invalidate(); } public void setScrollBarSkinRes(int scrollBarSkinRes) { mScrollBarSkinRes = scrollBarSkinRes; if (mRecyclerView != null) { QMUISkinHelper.refreshRVItemDecoration(mRecyclerView, this); } invalidate(); } public void setScrollBarSkinTintColorRes(int colorRes) { mScrollBarSkinTintColorRes = colorRes; if (mRecyclerView != null) { QMUISkinHelper.refreshRVItemDecoration(mRecyclerView, this); } invalidate(); } public void setDraggable(boolean draggable) { mIsDraggable = draggable; } public boolean isDraggable() { return mIsDraggable; } public void setEnableScrollBarFadeInOut(boolean enableScrollBarFadeInOut) { if (mEnableScrollBarFadeInOut != enableScrollBarFadeInOut) { mEnableScrollBarFadeInOut = enableScrollBarFadeInOut; if (!mEnableScrollBarFadeInOut) { mBeginAlpha = -1; mTargetAlpha = -1; mCurrentAlpha = 255; } else { if (mRecyclerView != null) { if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { mCurrentAlpha = 0; } } else { mCurrentAlpha = 0; } } invalidate(); } } public boolean isEnableScrollBarFadeInOut() { return mEnableScrollBarFadeInOut; } private void commonAttachToRecyclerView(@Nullable RecyclerView recyclerView) { if (mRecyclerView == recyclerView) { return; // nothing to do } if (mRecyclerView != null) { destroyCallbacks(); } mRecyclerView = recyclerView; if (recyclerView != null) { setupCallbacks(); QMUISkinHelper.refreshRVItemDecoration(recyclerView, this); } } public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { if (mStickySectionLayout != null) { mStickySectionLayout.removeDrawDecoration(this); mStickySectionLayout = null; } commonAttachToRecyclerView(recyclerView); } public void attachToStickSectionLayout(@Nullable QMUIStickySectionLayout stickySectionLayout) { if (mStickySectionLayout == stickySectionLayout) { return; // nothing to do } if (mStickySectionLayout != null) { mStickySectionLayout.removeDrawDecoration(this); } mStickySectionLayout = stickySectionLayout; if (stickySectionLayout != null) { stickySectionLayout.addDrawDecoration(this); commonAttachToRecyclerView(stickySectionLayout.getRecyclerView()); } } private void setupCallbacks() { mRecyclerView.addItemDecoration(this); mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnScrollListener(mScrollListener); } private void destroyCallbacks() { mRecyclerView.removeItemDecoration(this); mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); mRecyclerView.removeCallbacks(mFadeScrollBarAction); mRecyclerView.removeOnScrollListener(mScrollListener); } private void startDrag() { mIsInDragging = true; if (mScrollBarDrawable != null) { mScrollBarDrawable.setState(STATE_PRESSED); } if (mCallback != null) { mCallback.onDragStarted(); } if (mRecyclerView != null) { mRecyclerView.removeCallbacks(mFadeScrollBarAction); } invalidate(); } private void endDrag() { mIsInDragging = false; if (mScrollBarDrawable != null) { mScrollBarDrawable.setState(STATE_NORMAL); } if (mCallback != null) { mCallback.onDragEnd(); } invalidate(); } private void onDragging(RecyclerView recyclerView, Drawable drawable, int x, int y) { int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); int usefulSpace = getUsefulSpace(recyclerView) - (mIsVerticalScroll ? drawableHeight : drawableWidth); int useValue = mIsVerticalScroll ? y : x; float percent = (useValue - mStartMargin - mDragInnerStart) * 1f / usefulSpace; percent = QMUILangHelper.constrain(percent, 0f, 1f); if (mCallback != null) { mCallback.onDragToPercent(percent); } mPercent = percent; if (percent <= 0) { recyclerView.scrollToPosition(0); } else if (percent >= 1f) { RecyclerView.Adapter adapter = recyclerView.getAdapter(); if (adapter != null) { recyclerView.scrollToPosition(adapter.getItemCount() - 1); } } else { RecyclerView.Adapter adapter = recyclerView.getAdapter(); RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if(adapter != null && adapter.getItemCount() > MIN_COUNT_FOR_PERCENT_CALCULATE && layoutManager instanceof LinearLayoutManager){ ((LinearLayoutManager)layoutManager).scrollToPositionWithOffset((int) (adapter.getItemCount() * mPercent), 0); }else{ int range = getScrollRange(recyclerView); int offset = getCurrentOffset(recyclerView); int delta = (int) (range * mPercent - offset); if (mIsVerticalScroll) { recyclerView.scrollBy(0, delta); } else { recyclerView.scrollBy(delta, 0); } } } invalidate(); } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { if (mStickySectionLayout == null) { drawScrollBar(c, parent); } } @Override public void onDraw(@NonNull Canvas c, @NonNull QMUIStickySectionLayout parent) { } @Override public void onDrawOver(@NonNull Canvas c, @NonNull QMUIStickySectionLayout parent) { if (mRecyclerView != null) { drawScrollBar(c, mRecyclerView); } } private void drawScrollBar(@NonNull Canvas c, @NonNull RecyclerView recyclerView) { Drawable drawable = ensureScrollBar(recyclerView.getContext()); if (drawable == null || !needDrawScrollBar(recyclerView)) { return; } if (mTargetAlpha != -1 && mBeginAlpha != -1) { long transitionTime = System.currentTimeMillis() - mStartTransitionTime; long duration = mTransitionDuration * Math.abs(mTargetAlpha - mBeginAlpha) / 255; if (transitionTime >= duration) { mCurrentAlpha = mTargetAlpha; mTargetAlpha = -1; mBeginAlpha = -1; } else { mCurrentAlpha = (int) (mBeginAlpha + (mTargetAlpha - mBeginAlpha) * transitionTime * 1f / duration); recyclerView.postInvalidateOnAnimation(); } } drawable.setAlpha(mCurrentAlpha); if (!mIsInDragging) { mPercent = calculatePercent(recyclerView); } setScrollBarBounds(recyclerView, drawable); drawable.draw(c); } private int getUsefulSpace(@NonNull RecyclerView recyclerView) { if (mIsVerticalScroll) { return recyclerView.getHeight() - mStartMargin - mEndMargin; } return recyclerView.getWidth() - mStartMargin - mEndMargin; } private boolean needDrawScrollBar(RecyclerView recyclerView){ if(mIsVerticalScroll){ return recyclerView.canScrollVertically(-1) || recyclerView.canScrollVertically(1); } return recyclerView.canScrollHorizontally(-1) || recyclerView.canScrollHorizontally(1); } private void setScrollBarBounds(@NonNull RecyclerView recyclerView, @NonNull Drawable drawable) { int usefulSpace = getUsefulSpace(recyclerView); int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); int left, top; if (mIsVerticalScroll) { top = (int) ((usefulSpace - drawableHeight) * mPercent); left = mIsLocationInOppositeSide ? mInwardOffset : (recyclerView.getWidth() - drawableWidth - mInwardOffset); } else { left = (int) ((usefulSpace - drawableWidth) * mPercent); top = mIsLocationInOppositeSide ? mInwardOffset : (recyclerView.getHeight() - drawableHeight - mInwardOffset); } drawable.setBounds(left, top, left + drawableWidth, top + drawableHeight); } private int getScrollRange(@NonNull RecyclerView recyclerView) { if (mIsVerticalScroll) { return recyclerView.computeVerticalScrollRange() - recyclerView.getHeight(); } else { return recyclerView.computeHorizontalScrollRange() - recyclerView.getWidth(); } } private int getCurrentOffset(@NonNull RecyclerView recyclerView) { if (mIsVerticalScroll) { return recyclerView.computeVerticalScrollOffset(); } return recyclerView.computeHorizontalScrollOffset(); } private float calculatePercent(@NonNull RecyclerView recyclerView) { RecyclerView.Adapter adapter = recyclerView.getAdapter(); RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if(adapter != null && adapter.getItemCount() > MIN_COUNT_FOR_PERCENT_CALCULATE && layoutManager instanceof LinearLayoutManager){ LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; return linearLayoutManager.findFirstCompletelyVisibleItemPosition() * 1f / adapter.getItemCount(); } return QMUILangHelper.constrain(getCurrentOffset(recyclerView) * 1f / getScrollRange(recyclerView), 0f, 1f); } public Drawable ensureScrollBar(Context context) { if (mScrollBarDrawable == null) { setScrollBarDrawable( ContextCompat.getDrawable(context, R.drawable.qmui_icon_scroll_bar)); } return mScrollBarDrawable; } @Override public void handle(@NotNull @NonNull RecyclerView recyclerView, @NotNull @NonNull QMUISkinManager manager, int skinIndex, @NotNull @NonNull Resources.Theme theme) { if (mScrollBarSkinRes != 0) { mScrollBarDrawable = QMUIResHelper.getAttrDrawable( recyclerView.getContext(), theme, mScrollBarSkinRes); } else if (mScrollBarSkinTintColorRes != 0 && mScrollBarDrawable != null) { DrawableCompat.setTintList(mScrollBarDrawable, QMUIResHelper.getAttrColorStateList( recyclerView.getContext(), theme, mScrollBarSkinTintColorRes)); } invalidate(); } public interface Callback { void onDragStarted(); void onDragToPercent(float percent); void onDragEnd(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUIRVItemSwipeAction.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.recyclerView; import android.animation.Animator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.res.Resources; import android.graphics.Canvas; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.R; import java.util.ArrayList; import java.util.List; public class QMUIRVItemSwipeAction extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener { public static final int SWIPE_NONE = 0; public static final int SWIPE_LEFT = 1; public static final int SWIPE_RIGHT = 2; public static final int SWIPE_UP = 3; public static final int SWIPE_DOWN = 4; public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1; public static final int ANIMATION_TYPE_SWIPE_CANCEL = 2; public static final int ANIMATION_TYPE_SWIPE_ACTION = 3; private static final int ACTIVE_POINTER_ID_NONE = -1; private static final int PIXELS_PER_SECOND = 1000; private static final int SWIPE_TRIGGERED_IMMEDIATELY = -1; private static final String TAG = "QMUIRVItemSwipeAction"; private static final boolean DEBUG = false; /** * Views, whose state should be cleared after they are detached from RecyclerView. * This is necessary after swipe dismissing an item. We wait until animator finishes its job * to clean these views. */ final List mPendingCleanup = new ArrayList<>(); /** * Re-use array to calculate dx dy for a ViewHolder */ private final float[] mTmpPosition = new float[2]; /** * The reference coordinates for the action start. For drag & drop, this is the time long * press is completed vs for swipe, this is the initial touch point. */ float mInitialTouchX; float mInitialTouchY; long mDownTimeMillis = 0; /** * Set when ItemTouchHelper is assigned to a RecyclerView. */ private float mSwipeEscapeVelocity; /** * Set when ItemTouchHelper is assigned to a RecyclerView. */ private float mMaxSwipeVelocity; /** * The diff between the last event and initial touch. */ float mDx; float mDy; /** * The pointer we are tracking. */ int mActivePointerId = ACTIVE_POINTER_ID_NONE; /** * When a View is swiped and needs to go back to where it was, we create a Recover * Animation and animate it to its location using this custom Animator, instead of using * framework Animators. * Using framework animators has the side effect of clashing with ItemAnimator, creating * jumpy UIs. */ List mRecoverAnimations = new ArrayList<>(); private int mSlop; RecyclerView mRecyclerView; /** * Used for detecting fling swipe */ VelocityTracker mVelocityTracker; private long mPressTimeToSwipe = SWIPE_TRIGGERED_IMMEDIATELY; /** * The coordinates of the selected view at the time it is selected. We record these values * when action starts so that we can consistently position it even if LayoutManager moves the * View. */ float mSelectedStartX; float mSelectedStartY; int mSwipeDirection; private MotionEvent mCurrentDownEvent; private Runnable mLongPressToSwipe = new Runnable() { @Override public void run() { if (mCurrentDownEvent != null) { final int activePointerIndex = mCurrentDownEvent.findPointerIndex(mActivePointerId); if (activePointerIndex >= 0) { checkSelectForSwipe(mCurrentDownEvent.getAction(), mCurrentDownEvent, activePointerIndex, true); } } } }; /** * Currently selected view holder */ RecyclerView.ViewHolder mSelected = null; private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { if (DEBUG) { Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); } final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { if (mCurrentDownEvent != null) { mCurrentDownEvent.recycle(); } mCurrentDownEvent = MotionEvent.obtain(event); if (mPressTimeToSwipe > 0 && mSelected == null) { recyclerView.postDelayed(mLongPressToSwipe, mPressTimeToSwipe); } mActivePointerId = event.getPointerId(0); mInitialTouchX = event.getX(); mInitialTouchY = event.getY(); obtainVelocityTracker(); mDownTimeMillis = System.currentTimeMillis(); if (mSelected == null) { final RecoverAnimation animation = findAnimation(event); if (animation != null) { mInitialTouchX -= animation.mX; mInitialTouchY -= animation.mY; endRecoverAnimation(animation.mViewHolder, true); if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { mCallback.clearView(mRecyclerView, animation.mViewHolder); } select(animation.mViewHolder); updateDxDy(event, mSwipeDirection, 0); } } else { if (mSelected instanceof QMUISwipeViewHolder) { QMUISwipeViewHolder swipeViewHolder = (QMUISwipeViewHolder) mSelected; boolean isDownToAction = swipeViewHolder.checkDown(mInitialTouchX, mInitialTouchY); if (!isDownToAction) { if (hitTest(mSelected.itemView, mInitialTouchX, mInitialTouchY, mSelectedStartX + mDx, mSelectedStartY + mDy)) { mInitialTouchX -= mDx; mInitialTouchY -= mDy; } else { select(null); return true; } } else { mInitialTouchX -= mDx; mInitialTouchY -= mDy; } } } } else if (action == MotionEvent.ACTION_CANCEL) { mActivePointerId = ACTIVE_POINTER_ID_NONE; mRecyclerView.removeCallbacks(mLongPressToSwipe); select(null); } else if (action == MotionEvent.ACTION_UP) { mRecyclerView.removeCallbacks(mLongPressToSwipe); handleActionUp(event.getX(), event.getY(), mSlop); mActivePointerId = ACTIVE_POINTER_ID_NONE; } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { // in a non scroll orientation, if distance change is above threshold, we // can select the item final int index = event.findPointerIndex(mActivePointerId); if (DEBUG) { Log.d(TAG, "pointer index " + index); } if (index >= 0) { checkSelectForSwipe(action, event, index, false); } } if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } return mSelected != null; } @Override public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent event) { if (DEBUG) { Log.d(TAG, "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); } if (mVelocityTracker != null) { mVelocityTracker.addMovement(event); } if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return; } final int action = event.getActionMasked(); final int activePointerIndex = event.findPointerIndex(mActivePointerId); if (activePointerIndex >= 0) { checkSelectForSwipe(action, event, activePointerIndex, false); } RecyclerView.ViewHolder viewHolder = mSelected; if (viewHolder == null) { return; } switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { updateDxDy(event, mSwipeDirection, activePointerIndex); mRecyclerView.invalidate(); final float x = event.getX(activePointerIndex); final float y = event.getY(activePointerIndex); if (Math.abs(x - mInitialTouchX) > mSlop || Math.abs(y - mInitialTouchY) > mSlop) { mRecyclerView.removeCallbacks(mLongPressToSwipe); } } break; } case MotionEvent.ACTION_CANCEL: mRecyclerView.removeCallbacks(mLongPressToSwipe); select(null); if (mVelocityTracker != null) { mVelocityTracker.clear(); } mActivePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_UP: mRecyclerView.removeCallbacks(mLongPressToSwipe); handleActionUp(event.getX(), event.getY(), mSlop); if (mVelocityTracker != null) { mVelocityTracker.clear(); } mActivePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = event.getPointerId(newPointerIndex); updateDxDy(event, mSwipeDirection, pointerIndex); } break; } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (!disallowIntercept) { return; } select(null); } }; private Callback mCallback; private boolean mSwipeDeleteWhenOnlyOneAction = false; public QMUIRVItemSwipeAction(boolean swipeDeleteWhenOnlyOneAction, Callback callback) { mCallback = callback; mSwipeDeleteWhenOnlyOneAction = swipeDeleteWhenOnlyOneAction; } /** * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already * attached to a RecyclerView, it will first detach from the previous one. You can call this * method with {@code null} to detach it from the current RecyclerView. * * @param recyclerView The RecyclerView instance to which you want to add this helper or * {@code null} if you want to remove ItemTouchHelper from the current * RecyclerView. */ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { if (mRecyclerView == recyclerView) { return; // nothing to do } if (mRecyclerView != null) { destroyCallbacks(); } mRecyclerView = recyclerView; if (recyclerView != null) { final Resources resources = recyclerView.getResources(); mSwipeEscapeVelocity = resources.getDimension(R.dimen.qmui_rv_swipe_action_escape_velocity); mMaxSwipeVelocity = resources.getDimension(R.dimen.qmui_rv_swipe_action_escape_max_velocity); setupCallbacks(); } } public void setPressTimeToSwipe(long pressTimeToSwipe) { mPressTimeToSwipe = pressTimeToSwipe; } private void setupCallbacks() { ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); mSlop = vc.getScaledTouchSlop(); mRecyclerView.addItemDecoration(this); mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnChildAttachStateChangeListener(this); } private void destroyCallbacks() { mRecyclerView.removeItemDecoration(this); mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); mRecyclerView.removeOnChildAttachStateChangeListener(this); // clean all attached final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); } mRecoverAnimations.clear(); releaseVelocityTracker(); } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { float dx = 0, dy = 0; if (mSelected != null) { getSelectedDxDy(mTmpPosition); dx = mTmpPosition[0]; dy = mTmpPosition[1]; } mCallback.onDrawOver(c, parent, mSelected, mRecoverAnimations, dx, dy); } @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { float dx = 0, dy = 0; if (mSelected != null) { getSelectedDxDy(mTmpPosition); dx = mTmpPosition[0]; dy = mTmpPosition[1]; } mCallback.onDraw(c, parent, mSelected, mRecoverAnimations, dx, dy, mSwipeDirection); } @Override public void onChildViewAttachedToWindow(@NonNull View view) { } @Override public void onChildViewDetachedFromWindow(@NonNull View view) { final RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); if (holder == null) { return; } if (mSelected != null && holder == mSelected) { select(null); } else { endRecoverAnimation(holder, false); // this may push it into pending cleanup list. if (mPendingCleanup.remove(holder.itemView)) { mCallback.clearView(mRecyclerView, holder); } } } void updateDxDy(MotionEvent ev, int swipeDirection, int pointerIndex) { final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); // Calculate the distance moved if (swipeDirection == SWIPE_RIGHT) { mDx = Math.max(0, x - mInitialTouchX); mDy = 0; } else if (swipeDirection == SWIPE_LEFT) { mDx = Math.min(0, x - mInitialTouchX); mDy = 0; } else if (swipeDirection == SWIPE_DOWN) { mDx = 0; mDy = Math.max(0, y - mInitialTouchY); } else if (swipeDirection == SWIPE_UP) { mDx = 0; mDy = Math.min(0, y - mInitialTouchY); } } /** * Checks whether we should select a View for swiping. */ void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex, boolean isLongPressToSwipe) { if (mSelected != null || (mPressTimeToSwipe == SWIPE_TRIGGERED_IMMEDIATELY && action != MotionEvent.ACTION_MOVE)) { return; } if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { return; } final RecyclerView.ViewHolder vh = findSwipedView(motionEvent, isLongPressToSwipe); if (vh == null) { return; } int swipeDirection = mCallback.getSwipeDirection(mRecyclerView, vh); if (swipeDirection == SWIPE_NONE) { return; } if (mPressTimeToSwipe == SWIPE_TRIGGERED_IMMEDIATELY) { // mDx and mDy are only set in allowed directions. We use custom x/y here instead of // updateDxDy to avoid swiping if user moves more in the other direction final float x = motionEvent.getX(pointerIndex); final float y = motionEvent.getY(pointerIndex); // Calculate the distance moved final float dx = x - mInitialTouchX; final float dy = y - mInitialTouchY; // swipe target is chose w/o applying flags so it does not really check if swiping in that // direction is allowed. This why here, we use mDx mDy to check slope value again. final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (swipeDirection == SWIPE_LEFT) { if (absDx < mSlop || dx >= 0) { return; } } else if (swipeDirection == SWIPE_RIGHT) { if (absDx < mSlop || dx <= 0) { return; } } else if (swipeDirection == SWIPE_UP) { if (absDy < mSlop || dy >= 0) { return; } } else if (swipeDirection == SWIPE_DOWN) { if (absDy < mSlop || dy <= 0) { return; } } } else { if (mPressTimeToSwipe >= System.currentTimeMillis() - mDownTimeMillis) { return; } } mRecyclerView.removeCallbacks(mLongPressToSwipe); mDx = mDy = 0f; mActivePointerId = motionEvent.getPointerId(0); MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); vh.itemView.dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); select(vh); } public void clear() { select(null, false); } void handleActionUp(float x, float y, int touchSlop) { if (mSelected != null) { if (mSelected instanceof QMUISwipeViewHolder) { QMUISwipeViewHolder swipeViewHolder = (QMUISwipeViewHolder) mSelected; if (!swipeViewHolder.hasAction()) { select(null, true); } else if(swipeViewHolder.mSwipeActions.size() == 1 && mSwipeDeleteWhenOnlyOneAction){ if(mCallback.isOverThreshold(mRecyclerView, mSelected, mDx, mDy, mSwipeDirection)){ select(null, true); }else{ handleSwipeActionActionUp(swipeViewHolder, x, y, touchSlop); } } else { handleSwipeActionActionUp(swipeViewHolder, x, y, touchSlop); } } else { select(null, true); } } } void handleSwipeActionActionUp( QMUISwipeViewHolder swipeViewHolder, float x, float y, int touchSlop){ QMUISwipeAction action = swipeViewHolder.checkUp(x, y, touchSlop); if (action != null) { mCallback.onClickAction(this, mSelected, action); swipeViewHolder.clearTouchInfo(); return; } swipeViewHolder.clearTouchInfo(); final int swipeDir = checkSwipe(mSelected, mSwipeDirection, true); if (swipeDir == SWIPE_NONE) { select(null, true); } else { getSelectedDxDy(mTmpPosition); final float currentTranslateX = mTmpPosition[0]; final float currentTranslateY = mTmpPosition[1]; final float targetTranslateX, targetTranslateY; switch (swipeDir) { case SWIPE_LEFT: targetTranslateY = 0; targetTranslateX = -swipeViewHolder.mActionTotalWidth; break; case SWIPE_RIGHT: targetTranslateY = 0; targetTranslateX = swipeViewHolder.mActionTotalWidth; break; case SWIPE_UP: targetTranslateX = 0; targetTranslateY = -swipeViewHolder.mActionTotalHeight; break; case SWIPE_DOWN: targetTranslateX = 0; targetTranslateY = swipeViewHolder.mActionTotalHeight; break; default: targetTranslateX = 0; targetTranslateY = 0; } mDx += targetTranslateX - currentTranslateX; mDy += targetTranslateY - currentTranslateY; final RecoverAnimation rv = new RecoverAnimation(swipeViewHolder, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY, mCallback.getInterpolator(ANIMATION_TYPE_SWIPE_ACTION)); final long duration = mCallback.getAnimationDuration(mRecyclerView, ANIMATION_TYPE_SWIPE_ACTION, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); mRecoverAnimations.add(rv); rv.start(); mRecyclerView.invalidate(); } } void select(@Nullable RecyclerView.ViewHolder selected) { select(selected, false); } void select(@Nullable RecyclerView.ViewHolder selected, boolean isActionUp) { if (selected == mSelected) { return; } // prevent duplicate animations endRecoverAnimation(selected, true); boolean preventLayout = false; if (mSelected != null) { final RecyclerView.ViewHolder prevSelected = mSelected; if (prevSelected.itemView.getParent() != null) { endRecoverAnimation(prevSelected, true); final int swipeDir = isActionUp ? checkSwipe(mSelected, mSwipeDirection, false) : SWIPE_NONE; getSelectedDxDy(mTmpPosition); final float currentTranslateX = mTmpPosition[0]; final float currentTranslateY = mTmpPosition[1]; final float targetTranslateX, targetTranslateY; switch (swipeDir) { case SWIPE_LEFT: case SWIPE_RIGHT: targetTranslateY = 0; targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); break; case SWIPE_UP: case SWIPE_DOWN: targetTranslateX = 0; targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); break; default: targetTranslateX = 0; targetTranslateY = 0; } final int animType = swipeDir > 0 ? ANIMATION_TYPE_SWIPE_SUCCESS : ANIMATION_TYPE_SWIPE_CANCEL; if (swipeDir > 0) { mCallback.onStartSwipeAnimation(mSelected, swipeDir); } final RecoverAnimation rv = new RecoverAnimation(prevSelected, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY, mCallback.getInterpolator(ANIMATION_TYPE_SWIPE_ACTION)) { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (this.mOverridden) { return; } if (swipeDir == SWIPE_NONE) { // this is a drag or failed swipe. recover immediately mCallback.clearView(mRecyclerView, prevSelected); // full cleanup will happen on onDrawOver } else { // wait until remove animation is complete. mPendingCleanup.add(prevSelected.itemView); mIsPendingCleanup = true; if (swipeDir > 0) { // Animation might be ended by other animators during a layout. // We defer callback to avoid editing adapter during a layout. postDispatchSwipe(this, swipeDir); } } } }; final long duration = mCallback.getAnimationDuration(mRecyclerView, animType, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); mRecoverAnimations.add(rv); rv.start(); preventLayout = true; } else { mCallback.clearView(mRecyclerView, prevSelected); } mSelected = null; } if (selected != null) { mSwipeDirection = mCallback.getSwipeDirection(mRecyclerView, selected); mSelectedStartX = selected.itemView.getLeft(); mSelectedStartY = selected.itemView.getTop(); mSelected = selected; if (selected instanceof QMUISwipeViewHolder) { QMUISwipeViewHolder qmuiSwipeViewHolder = (QMUISwipeViewHolder) selected; qmuiSwipeViewHolder.setup(mSwipeDirection, mSwipeDeleteWhenOnlyOneAction); } } final ViewParent rvParent = mRecyclerView.getParent(); if (rvParent != null) { rvParent.requestDisallowInterceptTouchEvent(mSelected != null); } if (!preventLayout) { mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); } mCallback.onSelectedChanged(mSelected); mRecyclerView.invalidate(); } private void getSelectedDxDy(float[] outPosition) { if (mSwipeDirection == SWIPE_LEFT || mSwipeDirection == SWIPE_RIGHT) { outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); } else { outPosition[0] = mSelected.itemView.getTranslationX(); } if (mSwipeDirection == SWIPE_UP || mSwipeDirection == SWIPE_DOWN) { outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); } else { outPosition[1] = mSelected.itemView.getTranslationY(); } } void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { // wait until animations are complete. mRecyclerView.post(new Runnable() { @Override public void run() { if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() && !anim.mOverridden && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) { final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); // if animator is running or we have other active recover animations, we try // not to call onSwiped because DefaultItemAnimator is not good at merging // animations. Instead, we wait and batch. if ((animator == null || !animator.isRunning(null)) && !hasRunningRecoverAnim()) { mCallback.onSwiped(anim.mViewHolder, swipeDir); } else { mRecyclerView.post(this); } } } }); } boolean hasRunningRecoverAnim() { final int size = mRecoverAnimations.size(); for (int i = 0; i < size; i++) { if (!mRecoverAnimations.get(i).mEnded) { return true; } } return false; } private int checkSwipe(RecyclerView.ViewHolder viewHolder, int swipeDirection, boolean checkAction) { if (swipeDirection == SWIPE_LEFT || swipeDirection == SWIPE_RIGHT) { final int dirFlag = mDx > 0 ? SWIPE_RIGHT : SWIPE_LEFT; if (mVelocityTracker != null && mActivePointerId > -1) { mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); final int velDirFlag = xVelocity > 0f ? SWIPE_RIGHT : SWIPE_LEFT; final float absXVelocity = Math.abs(xVelocity); if (dirFlag == velDirFlag && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)) { return velDirFlag; } } float threshold; if (checkAction && viewHolder instanceof QMUISwipeViewHolder) { threshold = ((QMUISwipeViewHolder) viewHolder).mActionTotalWidth; } else { threshold = mRecyclerView.getWidth() * mCallback.getSwipeThreshold(viewHolder); } if (Math.abs(mDx) >= threshold) { return dirFlag; } } else if (swipeDirection == SWIPE_UP || swipeDirection == SWIPE_DOWN) { final int dirFlag = mDy > 0 ? SWIPE_DOWN : SWIPE_UP; if (mVelocityTracker != null && mActivePointerId > -1) { mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); final int velDirFlag = yVelocity > 0f ? SWIPE_DOWN : SWIPE_UP; final float absYVelocity = Math.abs(yVelocity); if (velDirFlag == dirFlag && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)) { return velDirFlag; } } float threshold; if (checkAction && viewHolder instanceof QMUISwipeViewHolder) { threshold = ((QMUISwipeViewHolder) viewHolder).mActionTotalHeight; } else { threshold = mRecyclerView.getHeight() * mCallback.getSwipeThreshold(viewHolder); } if (Math.abs(mDy) >= threshold) { return dirFlag; } } return SWIPE_NONE; } @Nullable private RecyclerView.ViewHolder findSwipedView(MotionEvent motionEvent, boolean isLongPressToSwipe) { final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mActivePointerId == ACTIVE_POINTER_ID_NONE || lm == null) { return null; } if (isLongPressToSwipe) { View child = findChildView(motionEvent); if (child == null) { return null; } return mRecyclerView.getChildViewHolder(child); } final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; final float absDx = Math.abs(dx); final float absDy = Math.abs(dy); if (absDx < mSlop && absDy < mSlop) { return null; } if (absDx > absDy && lm.canScrollHorizontally()) { return null; } else if (absDy > absDx && lm.canScrollVertically()) { return null; } View child = findChildView(motionEvent); if (child == null) { return null; } return mRecyclerView.getChildViewHolder(child); } void endRecoverAnimation(RecyclerView.ViewHolder viewHolder, boolean override) { final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); if (anim.mViewHolder == viewHolder) { anim.mOverridden |= override; if (!anim.mEnded) { anim.cancel(); } mRecoverAnimations.remove(i); return; } } } View findChildView(MotionEvent event) { // first check elevated views, if none, then call RV final float x = event.getX(); final float y = event.getY(); if (mSelected != null) { final View selectedView = mSelected.itemView; if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { return selectedView; } } for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); final View view = anim.mViewHolder.itemView; if (hitTest(view, x, y, view.getX(), view.getY())) { return view; } } return mRecyclerView.findChildViewUnder(x, y); } @Nullable RecoverAnimation findAnimation(MotionEvent event) { if (mRecoverAnimations.isEmpty()) { return null; } View target = findChildView(event); for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { final RecoverAnimation anim = mRecoverAnimations.get(i); if (anim.mViewHolder.itemView == target) { return anim; } } return null; } void obtainVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); } mVelocityTracker = VelocityTracker.obtain(); } private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private static boolean hitTest(View child, float x, float y, float left, float top) { return x >= left && x <= left + child.getWidth() && y >= top && y <= top + child.getHeight(); } private static class RecoverAnimation implements Animator.AnimatorListener { final float mStartDx; final float mStartDy; final float mTargetX; final float mTargetY; final RecyclerView.ViewHolder mViewHolder; private final ValueAnimator mValueAnimator; boolean mIsPendingCleanup; float mX; float mY; // if user starts touching a recovering view, we put it into interaction mode again, // instantly. boolean mOverridden = false; boolean mEnded = false; private float mFraction; RecoverAnimation(RecyclerView.ViewHolder viewHolder, float startDx, float startDy, float targetX, float targetY, TimeInterpolator interpolator) { mViewHolder = viewHolder; mStartDx = startDx; mStartDy = startDy; mTargetX = targetX; mTargetY = targetY; mValueAnimator = ValueAnimator.ofFloat(0f, 1f); mValueAnimator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setFraction(animation.getAnimatedFraction()); } }); mValueAnimator.setTarget(viewHolder.itemView); mValueAnimator.addListener(this); mValueAnimator.setInterpolator(interpolator); setFraction(0f); } public void setDuration(long duration) { mValueAnimator.setDuration(duration); } public void start() { mViewHolder.setIsRecyclable(false); mValueAnimator.start(); } public void cancel() { mValueAnimator.cancel(); } public void setFraction(float fraction) { mFraction = fraction; } /** * We run updates on onDraw method but use the fraction from animator callback. * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. */ public void update() { if (mStartDx == mTargetX) { mX = mViewHolder.itemView.getTranslationX(); } else { mX = mStartDx + mFraction * (mTargetX - mStartDx); } if (mStartDy == mTargetY) { mY = mViewHolder.itemView.getTranslationY(); } else { mY = mStartDy + mFraction * (mTargetY - mStartDy); } } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (!mEnded) { mViewHolder.setIsRecyclable(true); } mEnded = true; } @Override public void onAnimationCancel(Animator animation) { setFraction(1f); //make sure we recover the view's state. } @Override public void onAnimationRepeat(Animator animation) { } } public static abstract class Callback { public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { View view = viewHolder.itemView; view.setTranslationX(0); view.setTranslationY(0); if (viewHolder instanceof QMUISwipeViewHolder) { ((QMUISwipeViewHolder) viewHolder).clearTouchInfo(); } } public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return SWIPE_NONE; } public void onStartSwipeAnimation(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { } public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { } public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return .5f; } public float getSwipeEscapeVelocity(float defaultValue) { return defaultValue; } public float getSwipeVelocityThreshold(float defaultValue) { return defaultValue; } public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { return DEFAULT_SWIPE_ANIMATION_DURATION; } public void onSelectedChanged(RecyclerView.ViewHolder selected) { } public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { } public TimeInterpolator getInterpolator(int animationType) { return null; } void onDraw(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, List recoverAnimationList, float dX, float dY, int swipeDirection) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { final RecoverAnimation anim = recoverAnimationList.get(i); anim.update(); if (anim.mViewHolder == selected) { dX = anim.mX; dY = anim.mY; } else { final int count = c.save(); onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, false, swipeDirection); c.restoreToCount(count); } } if (selected != null) { final int count = c.save(); onChildDraw(c, parent, selected, dX, dY, true, swipeDirection); c.restoreToCount(count); } } void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, List recoverAnimationList, float dX, float dY) { final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { final RecoverAnimation anim = recoverAnimationList.get(i); final int count = c.save(); onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, false); c.restoreToCount(count); } if (selected != null) { final int count = c.save(); onChildDrawOver(c, parent, selected, dX, dY, true); c.restoreToCount(count); } boolean hasRunningAnimation = false; for (int i = recoverAnimSize - 1; i >= 0; i--) { final RecoverAnimation anim = recoverAnimationList.get(i); if (anim.mEnded && !anim.mIsPendingCleanup) { recoverAnimationList.remove(i); } else if (!anim.mEnded) { hasRunningAnimation = true; } } if (hasRunningAnimation) { parent.invalidate(); } } public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive) { } protected boolean isOverThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dx, float dy, int swipeDirection) { if (swipeDirection == SWIPE_LEFT || swipeDirection == SWIPE_RIGHT) { return Math.abs(dx) >= recyclerView.getWidth() * getSwipeThreshold(viewHolder); } return Math.abs(dy) >= recyclerView.getHeight() * getSwipeThreshold(viewHolder); } public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, boolean isCurrentlyActive, int swipeDirection) { View view = viewHolder.itemView; view.setTranslationX(dX); view.setTranslationY(dY); if (viewHolder instanceof QMUISwipeViewHolder) { if (swipeDirection != SWIPE_NONE) { ((QMUISwipeViewHolder) viewHolder).draw(c, isOverThreshold(recyclerView, viewHolder, dX, dY, swipeDirection), dX, dY); } } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUISwipeAction.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.recyclerView; import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import androidx.annotation.Nullable; import com.qmuiteam.qmui.QMUIInterpolatorStaticHolder; public class QMUISwipeAction { final String mText; Drawable mIcon; int mTextSize; Typeface mTypeface; int mSwipeDirectionMiniSize; int mIconTextGap; int mTextColor; int mTextColorAttr; int mBackgroundColor; int mBackgroundColorAttr; int mIconAttr; boolean mUseIconTint; int mPaddingStartEnd; int mOrientation; boolean mReverseDrawOrder; TimeInterpolator mSwipeMoveInterpolator; int mSwipePxPerMS; // inner use for layout and draw Paint paint; float contentWidth; float contentHeight; private QMUISwipeAction(ActionBuilder builder) { mText = builder.mText != null && builder.mText.length() > 0 ? builder.mText : null; mTextColor = builder.mTextColor; mTextSize = builder.mTextSize; mTypeface = builder.mTypeface; mTextColorAttr = builder.mTextColorAttr; mIcon = builder.mIcon; mIconAttr = builder.mIconAttr; mUseIconTint = builder.mUseIconTint; mIconTextGap = builder.mIconTextGap; mBackgroundColor = builder.mBackgroundColor; mBackgroundColorAttr = builder.mBackgroundColorAttr; mPaddingStartEnd = builder.mPaddingStartEnd; mSwipeDirectionMiniSize = builder.mSwipeDirectionMiniSize; mOrientation = builder.mOrientation; mReverseDrawOrder = builder.mReverseDrawOrder; mSwipeMoveInterpolator = builder.mSwipeMoveInterpolator; mSwipePxPerMS = builder.mSwipePxPerMS; paint = new Paint(); paint.setAntiAlias(true); paint.setTypeface(mTypeface); paint.setTextSize(mTextSize); Paint.FontMetrics fontMetrics = paint.getFontMetrics(); if (mIcon != null && mText != null) { mIcon.setBounds(0, 0, mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight()); if (mOrientation == ActionBuilder.HORIZONTAL) { contentWidth = mIcon.getIntrinsicWidth() + mIconTextGap + paint.measureText(mText); contentHeight = Math.max(fontMetrics.descent - fontMetrics.ascent, mIcon.getIntrinsicHeight()); } else { contentWidth = Math.max(mIcon.getIntrinsicWidth(), paint.measureText(mText)); contentHeight = fontMetrics.descent - fontMetrics.ascent + mIconTextGap + mIcon.getIntrinsicHeight(); } } else if (mIcon != null) { mIcon.setBounds(0, 0, mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight()); contentWidth = mIcon.getIntrinsicWidth(); contentHeight = mIcon.getIntrinsicHeight(); } else if (mText != null) { contentWidth = paint.measureText(mText); contentHeight = fontMetrics.descent - fontMetrics.ascent; } } public String getText() { return mText; } public int getTextColor() { return mTextColor; } public int getTextSize() { return mTextSize; } public Typeface getTypeface() { return mTypeface; } public int getTextColorAttr() { return mTextColorAttr; } public Drawable getIcon() { return mIcon; } public int getIconAttr() { return mIconAttr; } public boolean isUseIconTint() { return mUseIconTint; } public int getBackgroundColor() { return mBackgroundColor; } public int getBackgroundColorAttr() { return mBackgroundColorAttr; } public int getPaddingStartEnd() { return mPaddingStartEnd; } public int getIconTextGap() { return mIconTextGap; } public int getSwipeDirectionMiniSize() { return mSwipeDirectionMiniSize; } public int getOrientation() { return mOrientation; } protected void draw(Canvas canvas) { if (mText != null && mIcon != null) { if (mOrientation == ActionBuilder.HORIZONTAL) { if (mReverseDrawOrder) { canvas.drawText(mText, 0, (contentHeight - paint.descent() + paint.ascent()) / 2 - paint.ascent(), paint); canvas.save(); canvas.translate(contentWidth - mIcon.getIntrinsicWidth(), (contentHeight - mIcon.getIntrinsicHeight()) / 2); mIcon.draw(canvas); canvas.restore(); } else { canvas.save(); canvas.translate(0, (contentHeight - mIcon.getIntrinsicHeight()) / 2); mIcon.draw(canvas); canvas.restore(); canvas.drawText(mText, mIcon.getIntrinsicWidth() + mIconTextGap, (contentHeight - paint.descent() + paint.ascent()) / 2 - paint.ascent(), paint); } } else { float textWidth = paint.measureText(mText); if (mReverseDrawOrder) { canvas.drawText(mText, (contentWidth - textWidth) / 2, -paint.ascent(), paint); canvas.save(); canvas.translate( (contentWidth - mIcon.getIntrinsicWidth()) / 2, contentHeight - mIcon.getIntrinsicHeight()); mIcon.draw(canvas); canvas.restore(); } else { canvas.save(); canvas.translate((contentWidth - mIcon.getIntrinsicWidth()) / 2, 0); mIcon.draw(canvas); canvas.restore(); canvas.drawText(mText, (contentWidth - textWidth) / 2, contentHeight - paint.descent(), paint); } } } else if (mIcon != null) { mIcon.draw(canvas); } else if (mText != null) { canvas.drawText(mText, 0, -paint.ascent(), paint); } } public static class ActionBuilder { public static final int VERTICAL = 1; public static final int HORIZONTAL = 2; String mText; Drawable mIcon; int mTextSize; Typeface mTypeface; int mSwipeDirectionMiniSize; int mIconTextGap; int mTextColor; int mTextColorAttr = 0; int mBackgroundColor; int mBackgroundColorAttr = 0; int mIconAttr = 0; boolean mUseIconTint = false; int mPaddingStartEnd = 0; int mOrientation = VERTICAL; boolean mReverseDrawOrder = false; TimeInterpolator mSwipeMoveInterpolator = QMUIInterpolatorStaticHolder.ACCELERATE_INTERPOLATOR; int mSwipePxPerMS = 2; public ActionBuilder text(String text) { mText = text; return this; } public ActionBuilder textSize(int textSize) { mTextSize = textSize; return this; } public ActionBuilder textColor(int textColor) { mTextColor = textColor; return this; } public ActionBuilder typeface(Typeface typeface) { mTypeface = typeface; return this; } public ActionBuilder textColorAttr(int textColorAttr) { mTextColorAttr = textColorAttr; return this; } public ActionBuilder icon(@Nullable Drawable drawable) { mIcon = drawable == null ? null : drawable.mutate(); return this; } public ActionBuilder iconAttr(int iconAttr) { mIconAttr = iconAttr; return this; } public ActionBuilder useIconTint(boolean useIconTint) { mUseIconTint = useIconTint; return this; } public ActionBuilder backgroundColor(int backgroundColor) { mBackgroundColor = backgroundColor; return this; } public ActionBuilder backgroundColorAttr(int backgroundColorAttr) { mBackgroundColorAttr = backgroundColorAttr; return this; } public ActionBuilder paddingStartEnd(int paddingStartEnd) { mPaddingStartEnd = paddingStartEnd; return this; } public ActionBuilder iconTextGap(int iconTextGap) { mIconTextGap = iconTextGap; return this; } public ActionBuilder swipeDirectionMinSize(int minSize) { mSwipeDirectionMiniSize = minSize; return this; } public ActionBuilder orientation(int orientation) { mOrientation = orientation; return this; } public ActionBuilder reverseDrawOrder(boolean reverse) { mReverseDrawOrder = reverse; return this; } public ActionBuilder swipeMoveInterpolator(TimeInterpolator interpolator) { mSwipeMoveInterpolator = interpolator; return this; } public ActionBuilder swipePxPerMS(int swipePxPerMS){ mSwipePxPerMS = swipePxPerMS; return this; } public QMUISwipeAction build() { return new QMUISwipeAction(this); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/recyclerView/QMUISwipeViewHolder.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.recyclerView; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Paint; import android.view.View; import android.view.ViewParent; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.util.QMUIViewHelper; import java.util.ArrayList; import java.util.List; import static com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction.SWIPE_DOWN; import static com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction.SWIPE_LEFT; import static com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction.SWIPE_NONE; import static com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction.SWIPE_RIGHT; import static com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction.SWIPE_UP; public class QMUISwipeViewHolder extends RecyclerView.ViewHolder { List mSwipeActions; int mActionTotalWidth = 0; int mActionTotalHeight = 0; int mSetupDirection = SWIPE_NONE; ActionWrapper mCurrentTouchAction = null; float mActionDownX = 0; float mActionDownY = 0; private QMUISwipeViewHolder.ActionWrapper.Callback mCallback = new ActionWrapper.Callback() { @Override public void invalidate() { ViewParent viewParent = itemView.getParent(); if (viewParent instanceof RecyclerView) { ((RecyclerView) viewParent).invalidate(); } } }; public QMUISwipeViewHolder(@NonNull View itemView) { super(itemView); } public void addSwipeAction(QMUISwipeAction action) { if (mSwipeActions == null) { mSwipeActions = new ArrayList<>(); } ActionWrapper actionWrapper = new ActionWrapper(action, mCallback); mSwipeActions.add(actionWrapper); } public void clearActions(){ if(mSwipeActions != null){ mSwipeActions.clear(); } } public boolean hasAction() { return mSwipeActions != null && !mSwipeActions.isEmpty(); } public void clearTouchInfo() { mCurrentTouchAction = null; mActionDownY = -1; mActionDownX = -1; } void setup(int swipeDirection, boolean swipeDeleteIfOnlyOneAction) { mActionTotalWidth = 0; mActionTotalHeight = 0; if (mSwipeActions == null || mSwipeActions.isEmpty()) { return; } mSetupDirection = swipeDirection; for (ActionWrapper wrapper : mSwipeActions) { QMUISwipeAction action = wrapper.action; if (swipeDirection == SWIPE_LEFT || swipeDirection == SWIPE_RIGHT) { wrapper.measureWidth = Math.max(action.mSwipeDirectionMiniSize, action.contentWidth + 2 * action.mPaddingStartEnd); wrapper.measureHeight = itemView.getHeight(); mActionTotalWidth += wrapper.measureWidth; } else if (swipeDirection == SWIPE_UP || swipeDirection == SWIPE_DOWN) { wrapper.measureHeight = Math.max(action.mSwipeDirectionMiniSize, action.contentHeight + 2 * action.mPaddingStartEnd); wrapper.measureWidth = itemView.getWidth(); mActionTotalHeight += wrapper.measureHeight; } } if (mSwipeActions.size() == 1 && swipeDeleteIfOnlyOneAction) { mSwipeActions.get(0).swipeDeleteMode = true; } else { for (ActionWrapper wrapper : mSwipeActions) { wrapper.swipeDeleteMode = false; } } if (swipeDirection == SWIPE_LEFT) { int targetLeft = itemView.getRight() - mActionTotalWidth; for (ActionWrapper wrapper : mSwipeActions) { wrapper.initLeft = itemView.getRight(); wrapper.initTop = wrapper.targetTop = itemView.getTop(); wrapper.targetLeft = targetLeft; targetLeft += wrapper.measureWidth; } } else if (swipeDirection == SWIPE_RIGHT) { int targetLeft = 0; for (ActionWrapper wrapper : mSwipeActions) { wrapper.initLeft = itemView.getLeft() - wrapper.measureWidth; wrapper.initTop = wrapper.targetTop = itemView.getTop(); wrapper.targetLeft = targetLeft; targetLeft += wrapper.measureWidth; } } else if (swipeDirection == SWIPE_UP) { int targetTop = itemView.getBottom() - mActionTotalHeight; for (ActionWrapper wrapper : mSwipeActions) { wrapper.initLeft = wrapper.targetLeft = itemView.getLeft(); wrapper.initTop = itemView.getBottom(); wrapper.targetTop = targetTop; targetTop += wrapper.measureHeight; } } else if (swipeDirection == SWIPE_DOWN) { int targetTop = 0; for (ActionWrapper wrapper : mSwipeActions) { wrapper.initLeft = wrapper.targetLeft = itemView.getLeft(); wrapper.initTop = itemView.getTop() - wrapper.measureHeight; wrapper.targetTop = targetTop; targetTop += wrapper.measureHeight; } } } boolean checkDown(float x, float y) { for (ActionWrapper actionInfo : mSwipeActions) { if (actionInfo.hitTest(x, y)) { mCurrentTouchAction = actionInfo; mActionDownX = x; mActionDownY = y; return true; } } return false; } QMUISwipeAction checkUp(float x, float y, int touchSlop) { if (mCurrentTouchAction != null && mCurrentTouchAction.hitTest(x, y)) { if (Math.abs(x - mActionDownX) < touchSlop && Math.abs(y - mActionDownY) < touchSlop) { return mCurrentTouchAction.action; } } return null; } void draw(Canvas canvas, boolean overSwipeThreshold, float dx, float dy) { if (mSwipeActions == null || mSwipeActions.isEmpty()) { return; } if (mActionTotalWidth > 0) { float absDx = Math.abs(dx); if (absDx <= mActionTotalWidth) { float percent = absDx / mActionTotalWidth; for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.width = actionInfo.measureWidth; actionInfo.left = actionInfo.initLeft + (actionInfo.targetLeft - actionInfo.initLeft) * percent; } } else { float overDx = absDx - mActionTotalWidth; float eachOver = overDx / mSwipeActions.size(); float startLeft = dx > 0 ? itemView.getLeft() : itemView.getRight() + dx; for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.width = actionInfo.measureWidth + eachOver; actionInfo.left = startLeft; startLeft += actionInfo.width; } } } else { for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.width = actionInfo.measureWidth; actionInfo.left = actionInfo.initLeft; } } if (mActionTotalHeight > 0) { float absDy = Math.abs(dy); if (absDy <= mActionTotalHeight) { float percent = absDy / mActionTotalHeight; for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.height = actionInfo.measureHeight; actionInfo.top = actionInfo.initTop + (actionInfo.targetTop - actionInfo.initTop) * percent; } } else { float overDy = absDy - mActionTotalHeight; float eachOver = overDy / mSwipeActions.size(); float startTop = dy > 0 ? itemView.getTop() : itemView.getBottom() + dy; for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.height = actionInfo.measureHeight + eachOver + 0.5f; actionInfo.top = startTop; startTop += actionInfo.height; } } } else { for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.height = actionInfo.measureHeight; actionInfo.top = actionInfo.initTop; } } for (ActionWrapper actionInfo : mSwipeActions) { actionInfo.draw(canvas, overSwipeThreshold, mSetupDirection); } } static class ActionWrapper { static int SWIPE_DELETE_BEFORE = 0; static int SWIPE_DELETE_ANIMATING_TO_AFTER = 1; static int SWIPE_DELETE_ANIMATING_TO_BEFORE = 2; static int SWIPE_DELETE_AFTER = 3; static int MAX_SWIPE_MOVE_DURATION = 250; final QMUISwipeAction action; final Callback callback; float measureWidth; float measureHeight; float targetLeft; float targetTop; float initLeft; float initTop; float left; float top; float width; float height; boolean swipeDeleteMode = false; private int swipeDeleteState = SWIPE_DELETE_BEFORE; private float currentAnimationProgress = 0; private ValueAnimator animator; private ValueAnimator.AnimatorUpdateListener listener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentAnimationProgress = (float) animation.getAnimatedValue(); callback.invalidate(); } }; private float lastLeft = -1, lastTop = -1, animStartLeft = -1, animStartTop = -1; public ActionWrapper(@NonNull QMUISwipeAction action, @NonNull Callback callback) { this.action = action; this.callback = callback; } boolean hitTest(float x, float y) { return x > left && x < left + width && y > top && y < top + height; } void draw(Canvas canvas, boolean overSwipeThreshold, int direction) { canvas.save(); canvas.translate(left, top); action.paint.setStyle(Paint.Style.FILL); action.paint.setColor(action.mBackgroundColor); canvas.drawRect(0, 0, width, height, action.paint); if (!swipeDeleteMode) { canvas.translate((width - action.contentWidth) / 2f, (height - action.contentHeight) / 2); } else { float anchorLeft = getAnchorDrawLeft(direction); float anchorTop = getAnchorDrawTop(direction); float followLeft = getFollowDrawLeft(direction); float followTop = getFollowDrawTop(direction); float drawLeft, drawTop; if (!overSwipeThreshold) { if (swipeDeleteState == SWIPE_DELETE_BEFORE) { drawLeft = anchorLeft; drawTop = anchorTop; } else if (swipeDeleteState == SWIPE_DELETE_AFTER) { swipeDeleteState = SWIPE_DELETE_ANIMATING_TO_BEFORE; drawLeft = followLeft; drawTop = followTop; startAnimator(drawLeft, drawTop, anchorLeft, anchorTop, direction); } else if (swipeDeleteState == SWIPE_DELETE_ANIMATING_TO_AFTER) { swipeDeleteState = SWIPE_DELETE_ANIMATING_TO_BEFORE; drawLeft = lastLeft; drawTop = lastTop; startAnimator(drawLeft, drawTop, anchorLeft, anchorTop, direction); } else { if (isVer(direction)) { drawLeft = anchorLeft; drawTop = animStartTop + (anchorTop - animStartTop) * currentAnimationProgress; } else { drawLeft = animStartLeft + (anchorLeft - animStartLeft) * currentAnimationProgress; drawTop = anchorTop; } if (currentAnimationProgress >= 1f) { swipeDeleteState = SWIPE_DELETE_BEFORE; } } } else { if (swipeDeleteState == SWIPE_DELETE_AFTER) { drawLeft = followLeft; drawTop = followTop; } else if (swipeDeleteState == SWIPE_DELETE_ANIMATING_TO_BEFORE) { swipeDeleteState = SWIPE_DELETE_ANIMATING_TO_AFTER; drawLeft = lastLeft; drawTop = lastTop; startAnimator(drawLeft, drawTop, followLeft, followTop, direction); } else if (swipeDeleteState == SWIPE_DELETE_BEFORE) { swipeDeleteState = SWIPE_DELETE_ANIMATING_TO_AFTER; drawLeft = anchorLeft; drawTop = anchorTop; startAnimator(drawLeft, drawTop, followLeft, followTop, direction); } else { if (isVer(direction)) { drawLeft = followLeft; drawTop = animStartTop + (followTop - animStartTop) * currentAnimationProgress; } else { drawLeft = animStartLeft + (followLeft - animStartLeft) * currentAnimationProgress; drawTop = followTop; } if (currentAnimationProgress >= 1f) { swipeDeleteState = SWIPE_DELETE_AFTER; } } } canvas.translate(drawLeft - left, drawTop - top); lastLeft = drawLeft; lastTop = drawTop; } action.paint.setColor(action.mTextColor); action.draw(canvas); canvas.restore(); } private void startAnimator(float curLeft, float curTop, float targetLeft, float targetTop, int direction) { QMUIViewHelper.clearValueAnimator(animator); if (isVer(direction)) { animator = ValueAnimator.ofFloat(0, 1); animStartTop = curTop; } else { animator = ValueAnimator.ofFloat(0, 1); animStartLeft = curLeft; } float dis = isVer(direction) ? Math.abs(targetTop - curTop) : Math.abs(targetLeft - curLeft); int duration = Math.min(MAX_SWIPE_MOVE_DURATION, (int) (dis / action.mSwipePxPerMS)); animator.setDuration(duration); animator.setInterpolator(action.mSwipeMoveInterpolator); animator.addUpdateListener(listener); animator.start(); } private boolean isVer(int direction) { return direction == SWIPE_DOWN || direction == SWIPE_UP; } private float getAnchorDrawLeft(int direction) { if(direction == SWIPE_LEFT){ if(left > targetLeft){ return getFollowDrawLeft(direction); } }else if(direction == SWIPE_RIGHT){ if(left < targetLeft){ return getFollowDrawLeft(direction); } } return targetLeft + (measureWidth - action.contentWidth) / 2; } private float getAnchorDrawTop(int direction) { if(direction == SWIPE_UP){ if(top > targetTop){ return getFollowDrawTop(direction); } }else if(direction == SWIPE_DOWN){ if(top < targetTop){ return getFollowDrawTop(direction); } } return targetTop + (measureHeight - action.contentHeight) / 2; } private float getFollowDrawLeft(int direction) { float innerHor = (measureWidth - action.contentWidth) / 2; if (direction == SWIPE_LEFT) { return left + innerHor; } else if (direction == SWIPE_RIGHT) { return left + width - measureWidth + innerHor; } return left + (width - action.contentWidth) / 2f; } private float getFollowDrawTop(int direction) { float innerVer = (measureHeight - action.contentHeight) / 2; if (direction == SWIPE_UP) { return top + innerVer; } else if (direction == SWIPE_DOWN) { return top + height - measureHeight + innerVer; } return top + (height - action.contentHeight) / 2f; } interface Callback { void invalidate(); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinApplyListener.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.skin; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; public interface IQMUISkinApplyListener { void onApply(View view, int skinIndex, @NonNull Resources.Theme theme); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinDispatchInterceptor.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.skin; import android.content.res.Resources; import androidx.annotation.NonNull; public interface IQMUISkinDispatchInterceptor { boolean intercept(int skinIndex, @NonNull Resources.Theme theme); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinHandlerDecoration.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.skin; import android.content.res.Resources; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public interface IQMUISkinHandlerDecoration { void handle(@NonNull RecyclerView recyclerView, @NonNull QMUISkinManager manager, int skinIndex, @NonNull Resources.Theme theme); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinHandlerSpan.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.skin; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public interface IQMUISkinHandlerSpan { void handle(@NonNull View view, @NonNull QMUISkinManager manager, int skinIndex, @NonNull Resources.Theme theme); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/IQMUISkinHandlerView.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.skin; import android.content.res.Resources; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; public interface IQMUISkinHandlerView { void handle(@NonNull QMUISkinManager manager, int skinIndex, @NonNull Resources.Theme theme, @Nullable SimpleArrayMap attrs); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinHelper.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.skin; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIResHelper; public class QMUISkinHelper { public static QMUISkinValueBuilder sSkinValueBuilder = QMUISkinValueBuilder.acquire(); public static Resources.Theme getSkinTheme(@NonNull View view) { QMUISkinManager.ViewSkinCurrent current = QMUISkinManager.getViewSkinCurrent(view); Resources.Theme theme; if (current == null || current.index < 0) { theme = view.getContext().getTheme(); } else { theme = QMUISkinManager.of(current.managerName, view.getContext()).getTheme(current.index); } return theme; } public static int getSkinColor(@NonNull View view, int colorAttr) { return QMUIResHelper.getAttrColor(getSkinTheme(view), colorAttr); } public static ColorStateList getSkinColorStateList(@NonNull View view, int colorAttr) { return QMUIResHelper.getAttrColorStateList(view.getContext(), getSkinTheme(view), colorAttr); } @Nullable public static Drawable getSkinDrawable(@NonNull View view, int drawableAttr) { return QMUIResHelper.getAttrDrawable(view.getContext(), getSkinTheme(view), drawableAttr); } public static void setSkinValue(@NonNull View view, QMUISkinValueBuilder skinValueBuilder) { setSkinValue(view, skinValueBuilder.build()); } public static void setSkinValue(@NonNull View view, String value) { view.setTag(R.id.qmui_skin_value, value); refreshViewSkin(view); } @MainThread public static void setSkinValue(@NonNull View view, SkinWriter writer) { writer.write(sSkinValueBuilder); setSkinValue(view, sSkinValueBuilder.build()); sSkinValueBuilder.clear(); } public static void refreshRVItemDecoration(@NonNull RecyclerView view, IQMUISkinHandlerDecoration itemDecoration) { QMUISkinManager.ViewSkinCurrent skinCurrent = QMUISkinManager.getViewSkinCurrent(view); if (skinCurrent != null) { QMUISkinManager.of(skinCurrent.managerName, view.getContext()).refreshRecyclerDecoration(view, itemDecoration, skinCurrent.index); } } public static int getCurrentSkinIndex(@NonNull View view) { QMUISkinManager.ViewSkinCurrent viewSkinCurrent = QMUISkinManager.getViewSkinCurrent(view); if (viewSkinCurrent != null) { return viewSkinCurrent.index; } return QMUISkinManager.DEFAULT_SKIN; } public static void refreshViewSkin(@NonNull View view) { QMUISkinManager.ViewSkinCurrent skinCurrent = QMUISkinManager.getViewSkinCurrent(view); if (skinCurrent != null) { QMUISkinManager.of(skinCurrent.managerName, view.getContext()).refreshTheme(view, skinCurrent.index); } } public static void syncViewSkin(@NonNull View view, @NonNull View sourceView) { QMUISkinManager.ViewSkinCurrent source = QMUISkinManager.getViewSkinCurrent(sourceView); if (source != null) { QMUISkinManager.ViewSkinCurrent skin = QMUISkinManager.getViewSkinCurrent(view); if (!source.equals(skin)) { QMUISkinManager.of(source.managerName, view.getContext()).dispatch(view, source.index); } } } public static void setSkinDefaultProvider(@NonNull View view, IQMUISkinDefaultAttrProvider provider) { view.setTag(R.id.qmui_skin_default_attr_provider, provider); } public static void setSkinApplyListener(@NonNull View view, @Nullable IQMUISkinApplyListener listener) { view.setTag(R.id.qmui_skin_apply_listener, listener); } @Nullable public static IQMUISkinApplyListener getSkinApplyListener(@NonNull View view) { Object listener = view.getTag(R.id.qmui_skin_apply_listener); if (listener instanceof IQMUISkinApplyListener) { return (IQMUISkinApplyListener) listener; } return null; } public static void setIgnoreSkinApply(@NonNull View view, boolean ignore){ view.setTag(R.id.qmui_skin_ignore_apply, ignore); } public static void setInterceptSkinDispatch(@NonNull View view, boolean intercept){ view.setTag(R.id.qmui_skin_intercept_dispatch, intercept); } public static void warnRuleNotSupport(View view, String rule) { QMUILog.w("QMUISkinManager", view.getClass().getSimpleName() + " does't support " + rule); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinLayoutInflaterFactory.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.skin; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUILangHelper; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.util.HashMap; public class QMUISkinLayoutInflaterFactory implements LayoutInflater.Factory2 { private static final String TAG = "QMUISkin"; private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app.", "android.view." }; private static final HashMap sSuccessClassNamePrefixMap = new HashMap<>(); /** * LayoutInflater.createView(four args) is provided in Android P, but some ROM did't follow the official. */ private static boolean sCanUseCreateViewFourArguments = true; private static boolean sDidCheckLayoutInflaterCreateViewExitFourArgMethod = false; private Resources.Theme mEmptyTheme; private WeakReference mActivityWeakReference; private LayoutInflater mOriginLayoutInflater; public QMUISkinLayoutInflaterFactory(Activity activity, LayoutInflater originLayoutInflater) { mActivityWeakReference = new WeakReference<>(activity); mOriginLayoutInflater = originLayoutInflater; } public QMUISkinLayoutInflaterFactory cloneForLayoutInflaterIfNeeded(LayoutInflater layoutInflater){ if(mOriginLayoutInflater.getContext() == layoutInflater.getContext()){ return this; } return new QMUISkinLayoutInflaterFactory(mActivityWeakReference.get(), layoutInflater); } @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { Activity activity = mActivityWeakReference.get(); View view = null; if(activity instanceof AppCompatActivity){ view = ((AppCompatActivity)activity).getDelegate().createView(parent, name, context, attrs); } if(view == null){ try{ if (!name.contains(".")) { if(sSuccessClassNamePrefixMap.containsKey(name)){ view = mOriginLayoutInflater .createView(name, sSuccessClassNamePrefixMap.get(name), attrs); }else{ for (String prefix : sClassPrefixList) { try { view = mOriginLayoutInflater.createView(name, prefix, attrs); if (view != null) { sSuccessClassNamePrefixMap.put(name, prefix); break; } } catch (Exception ignored) { } } } }else{ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ if(!sDidCheckLayoutInflaterCreateViewExitFourArgMethod){ try{ LayoutInflater.class.getDeclaredMethod( "createView", Context.class, String.class, String.class, AttributeSet.class); }catch (Exception e){ sCanUseCreateViewFourArguments = false; } sDidCheckLayoutInflaterCreateViewExitFourArgMethod = true; } if(sCanUseCreateViewFourArguments){ view = mOriginLayoutInflater.createView(context, name, null, attrs); }else{ view = originCreateViewForLowSDK(name, context, attrs); } }else{ view = originCreateViewForLowSDK(name, context, attrs); } } }catch (ClassNotFoundException ignore){ }catch (Exception e){ QMUILog.e(TAG, "Failed to inflate view " + name + "; error: " + e.getMessage()); } } if (view != null) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); getSkinValueFromAttributeSet(view.getContext(), attrs, builder); if (!builder.isEmpty()) { QMUISkinHelper.setSkinValue(view, builder); } QMUISkinValueBuilder.release(builder); } return view; } private View originCreateViewForLowSDK(String name, Context context, AttributeSet attrs) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, InflateException, ClassNotFoundException { @SuppressLint("SoonBlockedPrivateApi") Field field = LayoutInflater.class.getDeclaredField("mConstructorArgs"); field.setAccessible(true); Object[] mConstructorArgs = (Object[]) field.get(mOriginLayoutInflater); Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; View view = mOriginLayoutInflater.createView(name, null, attrs); mConstructorArgs[0] = lastContext; return view; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return onCreateView(null, name, context, attrs); } public void getSkinValueFromAttributeSet(Context context, @Nullable AttributeSet attrs, QMUISkinValueBuilder builder) { // use a empty theme, so we can get the attr's own value, not it's ref value if(mEmptyTheme == null){ mEmptyTheme = context.getApplicationContext().getResources().newTheme(); } TypedArray a = mEmptyTheme.obtainStyledAttributes(attrs, R.styleable.QMUISkinDef, 0, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); String name = a.getString(attr); if (QMUILangHelper.isNullOrEmpty(name)) { continue; } if (name.startsWith("?")) { name = name.substring(1); } int id = context.getResources().getIdentifier( name, "attr", context.getPackageName()); if (id == 0) { continue; } if (attr == R.styleable.QMUISkinDef_qmui_skin_background) { builder.background(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_alpha) { builder.alpha(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_border) { builder.border(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_text_color) { builder.textColor(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_second_text_color) { builder.secondTextColor(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_src) { builder.src(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_tint_color) { builder.tintColor(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_separator_top) { builder.topSeparator(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_separator_right) { builder.rightSeparator(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_separator_bottom) { builder.bottomSeparator(id); } else if (attr == R.styleable.QMUISkinDef_qmui_skin_separator_left) { builder.leftSeparator(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_bg_tint_color) { builder.bgTintColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_progress_color){ builder.progressColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_underline){ builder.underline(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_more_bg_color){ builder.moreBgColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_more_text_color){ builder.moreTextColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_hint_color){ builder.hintColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_text_compound_tint_color){ builder.textCompoundTintColor(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_text_compound_src_left){ builder.textCompoundLeftSrc(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_text_compound_src_top){ builder.textCompoundTopSrc(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_text_compound_src_right){ builder.textCompoundRightSrc(id); }else if(attr == R.styleable.QMUISkinDef_qmui_skin_text_compound_src_bottom){ builder.textCompoundBottomSrc(id); } } a.recycle(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinManager.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.skin; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.res.Resources; import android.os.Trace; import android.text.Spanned; import android.util.ArrayMap; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; import android.widget.PopupWindow; import android.widget.TextView; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.ViewPager; import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.annotation.QMUISkinListenWithHierarchyChange; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.skin.handler.IQMUISkinRuleHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleAlphaHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleBackgroundHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleBgTintColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleBorderHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleHintColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleMoreBgColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleMoreTextColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleProgressColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleSeparatorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleSrcHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleTextColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleTextCompoundSrcHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleTextCompoundTintColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleTintColorHandler; import com.qmuiteam.qmui.skin.handler.QMUISkinRuleUnderlineHandler; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; public final class QMUISkinManager { private static final String TAG = "QMUISkinManager"; public static final int DEFAULT_SKIN = -1; private static final String[] EMPTY_ITEMS = new String[]{}; private static ArrayMap sInstances = new ArrayMap<>(); private static final String DEFAULT_NAME = "default"; public static final DispatchListenStrategySelector DEFAULT_DISPATCH_LISTEN_STRATEGY_SELECTOR = new DispatchListenStrategySelector() { @NonNull @Override public DispatchListenStrategy select(@NonNull ViewGroup viewGroup) { if (viewGroup instanceof RecyclerView || viewGroup instanceof ViewPager || viewGroup instanceof AdapterView || viewGroup.getClass().isAnnotationPresent(QMUISkinListenWithHierarchyChange.class)) { return DispatchListenStrategy.LISTEN_ON_HIERARCHY_CHANGE; } return DispatchListenStrategy.LISTEN_ON_LAYOUT; } }; private static DispatchListenStrategySelector sDispatchListenStrategySelector = DEFAULT_DISPATCH_LISTEN_STRATEGY_SELECTOR; public static void setDispatchListenStrategySelector(DispatchListenStrategySelector dispatchListenStrategySelector) { if (dispatchListenStrategySelector == null) { sDispatchListenStrategySelector = DEFAULT_DISPATCH_LISTEN_STRATEGY_SELECTOR; } else { sDispatchListenStrategySelector = dispatchListenStrategySelector; } } @MainThread public static QMUISkinManager defaultInstance(Context context) { context = context.getApplicationContext(); return of(DEFAULT_NAME, context.getResources(), context.getPackageName()); } @MainThread public static QMUISkinManager of(String name, Resources resources, String packageName) { QMUISkinManager instance = sInstances.get(name); if (instance == null) { instance = new QMUISkinManager(name, resources, packageName); sInstances.put(name, instance); } return instance; } @MainThread public static QMUISkinManager of(String name, Context context) { context = context.getApplicationContext(); return of(name, context.getResources(), context.getPackageName()); } //============================================================================================== private String mName; private Resources mResources; private String mPackageName; private SparseArray mSkins = new SparseArray<>(); private static HashMap sRuleHandlers = new HashMap<>(); private static HashMap sStyleIdThemeMap = new HashMap<>(); private boolean mIsInSkinChangeDispatch = false; static { sRuleHandlers.put(QMUISkinValueBuilder.BACKGROUND, new QMUISkinRuleBackgroundHandler()); IQMUISkinRuleHandler textColorHandler = new QMUISkinRuleTextColorHandler(); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COLOR, textColorHandler); sRuleHandlers.put(QMUISkinValueBuilder.SECOND_TEXT_COLOR, textColorHandler); sRuleHandlers.put(QMUISkinValueBuilder.SRC, new QMUISkinRuleSrcHandler()); sRuleHandlers.put(QMUISkinValueBuilder.BORDER, new QMUISkinRuleBorderHandler()); IQMUISkinRuleHandler separatorHandler = new QMUISkinRuleSeparatorHandler(); sRuleHandlers.put(QMUISkinValueBuilder.TOP_SEPARATOR, separatorHandler); sRuleHandlers.put(QMUISkinValueBuilder.RIGHT_SEPARATOR, separatorHandler); sRuleHandlers.put(QMUISkinValueBuilder.BOTTOM_SEPARATOR, separatorHandler); sRuleHandlers.put(QMUISkinValueBuilder.LEFT_SEPARATOR, separatorHandler); sRuleHandlers.put(QMUISkinValueBuilder.TINT_COLOR, new QMUISkinRuleTintColorHandler()); sRuleHandlers.put(QMUISkinValueBuilder.ALPHA, new QMUISkinRuleAlphaHandler()); sRuleHandlers.put(QMUISkinValueBuilder.BG_TINT_COLOR, new QMUISkinRuleBgTintColorHandler()); sRuleHandlers.put(QMUISkinValueBuilder.PROGRESS_COLOR, new QMUISkinRuleProgressColorHandler()); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COMPOUND_TINT_COLOR, new QMUISkinRuleTextCompoundTintColorHandler()); IQMUISkinRuleHandler textCompoundSrcHandler = new QMUISkinRuleTextCompoundSrcHandler(); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COMPOUND_LEFT_SRC, textCompoundSrcHandler); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COMPOUND_TOP_SRC, textCompoundSrcHandler); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COMPOUND_RIGHT_SRC, textCompoundSrcHandler); sRuleHandlers.put(QMUISkinValueBuilder.TEXT_COMPOUND_BOTTOM_SRC, textCompoundSrcHandler); sRuleHandlers.put(QMUISkinValueBuilder.HINT_COLOR, new QMUISkinRuleHintColorHandler()); sRuleHandlers.put(QMUISkinValueBuilder.UNDERLINE, new QMUISkinRuleUnderlineHandler()); sRuleHandlers.put(QMUISkinValueBuilder.MORE_TEXT_COLOR, new QMUISkinRuleMoreTextColorHandler()); sRuleHandlers.put(QMUISkinValueBuilder.MORE_BG_COLOR, new QMUISkinRuleMoreBgColorHandler()); } public static void setRuleHandler(String name, IQMUISkinRuleHandler handler) { sRuleHandlers.put(name, handler); } // Actually, ViewGroup.OnHierarchyChangeListener is a better choice, but it only has a setter. // Add child will trigger onLayoutChange private static View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { @Override public void onLayoutChange( View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (v instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) v; int childCount = viewGroup.getChildCount(); if (childCount > 0) { ViewSkinCurrent current = getViewSkinCurrent(viewGroup); if (current != null) { View child; for (int i = 0; i < childCount; i++) { child = viewGroup.getChildAt(i); ViewSkinCurrent childTheme = getViewSkinCurrent(child); if (!current.equals(childTheme)) { of(current.managerName, child.getContext()).dispatch(child, current.index); } } } } } } }; private static ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener = new ViewGroup.OnHierarchyChangeListener() { @Override public void onChildViewAdded(View parent, View child) { ViewSkinCurrent current = getViewSkinCurrent(parent); if (current != null) { ViewSkinCurrent childTheme = getViewSkinCurrent(child); if (!current.equals(childTheme)) { of(current.managerName, child.getContext()).dispatch(child, current.index); } } } @Override public void onChildViewRemoved(View parent, View child) { } }; public QMUISkinManager(String name, Resources resources, String packageName) { mName = name; mResources = resources; mPackageName = packageName; } public String getName() { return mName; } @Nullable public Resources.Theme getTheme(int skinIndex) { SkinItem skinItem = mSkins.get(skinIndex); if (skinItem != null) { return skinItem.getTheme(); } return null; } @Nullable public Resources.Theme getCurrentTheme() { SkinItem skinItem = mSkins.get(mCurrentSkin); if (skinItem != null) { return skinItem.getTheme(); } return null; } @MainThread public void addSkin(int index, int styleRes) { if (index <= 0) { throw new IllegalArgumentException("index must greater than 0"); } SkinItem skinItem = mSkins.get(index); if (skinItem != null) { if (skinItem.getStyleRes() == styleRes) { return; } throw new RuntimeException("already exist the theme item for " + index); } skinItem = new SkinItem(styleRes); mSkins.append(index, skinItem); } static ViewSkinCurrent getViewSkinCurrent(View view) { Object current = view.getTag(R.id.qmui_skin_current); if (current instanceof ViewSkinCurrent) { return (ViewSkinCurrent) current; } return null; } public void dispatch(View view, int skinIndex) { if (view == null) { return; } if (QMUIConfig.DEBUG) { Trace.beginSection("QMUISkin::dispatch"); } SkinItem skinItem = mSkins.get(skinIndex); Resources.Theme theme; if (skinItem == null) { if (skinIndex != DEFAULT_SKIN) { throw new IllegalArgumentException("The skin " + skinIndex + " does not exist"); } theme = view.getContext().getTheme(); } else { theme = skinItem.getTheme(); } runDispatch(view, skinIndex, theme); if (QMUIConfig.DEBUG) { Trace.endSection(); } } private void runDispatch(@NonNull View view, int skinIndex, Resources.Theme theme) { ViewSkinCurrent currentTheme = getViewSkinCurrent(view); if (currentTheme != null && currentTheme.index == skinIndex && Objects.equals(currentTheme.managerName, mName)) { return; } view.setTag(R.id.qmui_skin_current, new ViewSkinCurrent(mName, skinIndex)); if (view instanceof IQMUISkinDispatchInterceptor) { if (((IQMUISkinDispatchInterceptor) view).intercept(skinIndex, theme)) { return; } } Object interceptTag = view.getTag(R.id.qmui_skin_intercept_dispatch); if (interceptTag instanceof Boolean && ((Boolean) interceptTag)) { return; } Object ignoreApplyTag = view.getTag(R.id.qmui_skin_ignore_apply); boolean ignoreApply = ignoreApplyTag instanceof Boolean && ((Boolean) ignoreApplyTag); if (!ignoreApply) { applyTheme(view, skinIndex, theme); } if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; if (sDispatchListenStrategySelector.select(viewGroup) == DispatchListenStrategy.LISTEN_ON_HIERARCHY_CHANGE) { viewGroup.setOnHierarchyChangeListener(mOnHierarchyChangeListener); } else { viewGroup.addOnLayoutChangeListener(mOnLayoutChangeListener); } for (int i = 0; i < viewGroup.getChildCount(); i++) { runDispatch(viewGroup.getChildAt(i), skinIndex, theme); } } else if (!ignoreApply && ((view instanceof TextView) || (view instanceof QMUIQQFaceView))) { CharSequence text; if (view instanceof TextView) { text = ((TextView) view).getText(); } else { text = ((QMUIQQFaceView) view).getText(); } if (text instanceof Spanned) { IQMUISkinHandlerSpan[] spans = ((Spanned) text).getSpans(0, text.length(), IQMUISkinHandlerSpan.class); if (spans != null) { for (int i = 0; i < spans.length; i++) { spans[i].handle(view, this, skinIndex, theme); } } view.invalidate(); } } } private void applyTheme(@NonNull View view, int skinIndex, Resources.Theme theme) { SimpleArrayMap attrs = getSkinAttrs(view); try { if (view instanceof IQMUISkinHandlerView) { ((IQMUISkinHandlerView) view).handle(this, skinIndex, theme, attrs); } else { defaultHandleSkinAttrs(view, theme, attrs); } Object skinApplyListener = view.getTag(R.id.qmui_skin_apply_listener); if (skinApplyListener instanceof IQMUISkinApplyListener) { ((IQMUISkinApplyListener) skinApplyListener).onApply(view, skinIndex, theme); } if (view instanceof RecyclerView) { RecyclerView recyclerView = (RecyclerView) view; int itemDecorationCount = recyclerView.getItemDecorationCount(); for (int i = 0; i < itemDecorationCount; i++) { RecyclerView.ItemDecoration itemDecoration = recyclerView.getItemDecorationAt(i); if (itemDecoration instanceof IQMUISkinHandlerDecoration) { ((IQMUISkinHandlerDecoration) itemDecoration).handle(recyclerView, this, skinIndex, theme); } } } } catch (Throwable throwable) { QMUILog.printErrStackTrace(TAG, throwable, "catch error when apply theme: " + view.getClass().getSimpleName() + "; " + skinIndex + "; attrs = " + (attrs == null ? "null" : attrs.toString())); } } void refreshRecyclerDecoration(@NonNull RecyclerView recyclerView, @NonNull IQMUISkinHandlerDecoration decoration, int skinIndex) { SkinItem skinItem = mSkins.get(skinIndex); if (skinItem != null) { decoration.handle(recyclerView, this, skinIndex, skinItem.getTheme()); } } void refreshTheme(@NonNull View view, int skinIndex) { SkinItem skinItem = mSkins.get(skinIndex); if (skinItem != null) { applyTheme(view, skinIndex, skinItem.getTheme()); } } public void defaultHandleSkinAttrs(@NonNull View view, Resources.Theme theme, @Nullable SimpleArrayMap attrs) { if (attrs != null) { for (int i = 0; i < attrs.size(); i++) { String key = attrs.keyAt(i); Integer attr = attrs.valueAt(i); if (attr == null) { continue; } defaultHandleSkinAttr(view, theme, key, attr); } } } public void defaultHandleSkinAttr(View view, Resources.Theme theme, String name, int attr) { if (attr == 0) { return; } IQMUISkinRuleHandler handler = sRuleHandlers.get(name); if (handler == null) { QMUILog.w(TAG, "Do not find handler for skin attr name: " + name); return; } handler.handle(this, view, theme, name, attr); } @Nullable private SimpleArrayMap getSkinAttrs(View view) { String skinValue = (String) view.getTag(R.id.qmui_skin_value); String[] items; if (skinValue == null || skinValue.isEmpty()) { items = EMPTY_ITEMS; } else { items = skinValue.split("[|]"); } SimpleArrayMap attrs = null; if (view instanceof IQMUISkinDefaultAttrProvider) { SimpleArrayMap defaultAttrs = ((IQMUISkinDefaultAttrProvider) view).getDefaultSkinAttrs(); if (defaultAttrs != null && !defaultAttrs.isEmpty()) { attrs = new SimpleArrayMap<>(defaultAttrs); } } IQMUISkinDefaultAttrProvider provider = (IQMUISkinDefaultAttrProvider) view.getTag( R.id.qmui_skin_default_attr_provider); if (provider != null) { SimpleArrayMap providedAttrs = provider.getDefaultSkinAttrs(); if (providedAttrs != null && !providedAttrs.isEmpty()) { if (attrs != null) { attrs.putAll(providedAttrs); } else { attrs = new SimpleArrayMap<>(providedAttrs); } } } if (attrs == null) { if (items.length <= 0) { return null; } attrs = new SimpleArrayMap<>(items.length); } for (String item : items) { String[] kv = item.split(":"); if (kv.length != 2) { continue; } String key = kv[0].trim(); if (QMUILangHelper.isNullOrEmpty(key)) { continue; } int attr = getAttrFromName(kv[1].trim()); if (attr == 0) { QMUILog.w(TAG, "Failed to get attr id from name: " + kv[1]); continue; } attrs.put(key, attr); } return attrs; } public int getAttrFromName(String attrName) { return mResources.getIdentifier(attrName, "attr", mPackageName); } class SkinItem { private int styleRes; SkinItem(int styleRes) { this.styleRes = styleRes; } public int getStyleRes() { return styleRes; } @NonNull Resources.Theme getTheme() { Resources.Theme theme = sStyleIdThemeMap.get(styleRes); if (theme == null) { theme = mResources.newTheme(); theme.applyStyle(styleRes, true); sStyleIdThemeMap.put(styleRes, theme); } return theme; } } // ===================================================================================== private int mCurrentSkin = DEFAULT_SKIN; private final List> mSkinObserverList = new ArrayList<>(); private final List mSkinChangeListeners = new ArrayList<>(); public void register(@NonNull Activity activity) { if (!containSkinObserver(activity)) { mSkinObserverList.add(new WeakReference<>(activity)); } dispatch(activity.findViewById(Window.ID_ANDROID_CONTENT), mCurrentSkin); } public void unRegister(@NonNull Activity activity) { removeSkinObserver(activity); } public void register(@NonNull Fragment fragment) { if (!containSkinObserver(fragment)) { mSkinObserverList.add(new WeakReference<>(fragment)); } dispatch(fragment.getView(), mCurrentSkin); } public void unRegister(@NonNull Fragment fragment) { removeSkinObserver(fragment); } public void register(@NonNull View view) { if (!containSkinObserver(view)) { mSkinObserverList.add(new WeakReference<>(view)); } dispatch(view, mCurrentSkin); } public void unRegister(@NonNull View view) { removeSkinObserver(view); } public void register(@NonNull Dialog dialog) { if (!containSkinObserver(dialog)) { mSkinObserverList.add(new WeakReference<>(dialog)); } Window window = dialog.getWindow(); if (window != null) { dispatch(window.getDecorView(), mCurrentSkin); } } public void unRegister(@NonNull Dialog dialog) { removeSkinObserver(dialog); } public void register(@NonNull PopupWindow popupWindow) { if (!containSkinObserver(popupWindow)) { mSkinObserverList.add(new WeakReference<>(popupWindow)); } dispatch(popupWindow.getContentView(), mCurrentSkin); } public void unRegister(@NonNull PopupWindow popupWindow) { removeSkinObserver(popupWindow); } public void register(@NonNull Window window) { if (!containSkinObserver(window)) { mSkinObserverList.add(new WeakReference<>(window)); } dispatch(window.getDecorView(), mCurrentSkin); } public void unRegister(@NonNull Window window) { removeSkinObserver(window); } private void removeSkinObserver(Object object) { for (int i = mSkinObserverList.size() - 1; i >= 0; i--) { Object item = mSkinObserverList.get(i).get(); if (item == object) { mSkinObserverList.remove(i); return; } else if (item == null) { mSkinObserverList.remove(i); } } } private boolean containSkinObserver(Object object) { //reverse order for remove for (int i = mSkinObserverList.size() - 1; i >= 0; i--) { Object item = mSkinObserverList.get(i).get(); if (item == object) { return true; } else if (item == null) { mSkinObserverList.remove(i); } } return false; } @MainThread public void changeSkin(int index) { if (mCurrentSkin == index) { return; } int oldIndex = mCurrentSkin; mCurrentSkin = index; mIsInSkinChangeDispatch = true; for (int i = mSkinObserverList.size() - 1; i >= 0; i--) { Object item = mSkinObserverList.get(i).get(); if (item == null) { mSkinObserverList.remove(i); } else { if (item instanceof Activity) { Activity activity = (Activity) item; activity.getWindow().setBackgroundDrawable(QMUIResHelper.getAttrDrawable( activity, mSkins.get(index).getTheme(), R.attr.qmui_skin_support_activity_background)); dispatch(activity.findViewById(Window.ID_ANDROID_CONTENT), index); } else if (item instanceof Fragment) { dispatch(((Fragment) item).getView(), index); } else if (item instanceof Dialog) { Window window = ((Dialog) item).getWindow(); if (window != null) { dispatch(window.getDecorView(), index); } } else if (item instanceof PopupWindow) { dispatch(((PopupWindow) item).getContentView(), index); } else if (item instanceof Window) { dispatch(((Window) item).getDecorView(), index); } else if (item instanceof View) { dispatch((View) item, index); } } } for (int i = mSkinChangeListeners.size() - 1; i >= 0; i--) { OnSkinChangeListener item = mSkinChangeListeners.get(i); item.onSkinChange(this, oldIndex, mCurrentSkin); } mIsInSkinChangeDispatch = false; } @MainThread public void addSkinChangeListener(@NonNull OnSkinChangeListener listener) { if (mIsInSkinChangeDispatch) { throw new RuntimeException("Can not add skinChangeListener while dispatching"); } mSkinChangeListeners.add(listener); } public void removeSkinChangeListener(@NonNull OnSkinChangeListener listener) { if (mIsInSkinChangeDispatch) { throw new RuntimeException("Can not add skinChangeListener while dispatching"); } mSkinChangeListeners.remove(listener); } public int getCurrentSkin() { return mCurrentSkin; } public interface OnSkinChangeListener { void onSkinChange(QMUISkinManager skinManager, int oldSkin, int newSkin); } class ViewSkinCurrent { String managerName; int index; ViewSkinCurrent(String managerName, int index) { this.managerName = managerName; this.index = index; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ViewSkinCurrent that = (ViewSkinCurrent) o; return index == that.index && Objects.equals(managerName, that.managerName); } @Override public int hashCode() { return Objects.hash(managerName, index); } } public interface DispatchListenStrategySelector { @NonNull DispatchListenStrategy select(@NonNull ViewGroup viewGroup); } public enum DispatchListenStrategy { LISTEN_ON_LAYOUT, LISTEN_ON_HIERARCHY_CHANGE } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/QMUISkinValueBuilder.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.skin; import java.util.HashMap; import java.util.LinkedList; import androidx.annotation.NonNull; public class QMUISkinValueBuilder { public static final String BACKGROUND = "background"; public static final String TEXT_COLOR = "textColor"; public static final String HINT_COLOR = "hintColor"; public static final String SECOND_TEXT_COLOR = "secondTextColor"; public static final String SRC = "src"; public static final String BORDER = "border"; public static final String TOP_SEPARATOR = "topSeparator"; public static final String BOTTOM_SEPARATOR = "bottomSeparator"; public static final String RIGHT_SEPARATOR = "rightSeparator"; public static final String LEFT_SEPARATOR = "LeftSeparator"; public static final String ALPHA = "alpha"; public static final String TINT_COLOR = "tintColor"; public static final String BG_TINT_COLOR = "bgTintColor"; public static final String PROGRESS_COLOR = "progressColor"; public static final String TEXT_COMPOUND_TINT_COLOR = "tcTintColor"; public static final String TEXT_COMPOUND_LEFT_SRC = "tclSrc"; public static final String TEXT_COMPOUND_RIGHT_SRC = "tcrSrc"; public static final String TEXT_COMPOUND_TOP_SRC = "tctSrc"; public static final String TEXT_COMPOUND_BOTTOM_SRC = "tcbSrc"; public static final String UNDERLINE = "underline"; public static final String MORE_TEXT_COLOR = "moreTextColor"; public static final String MORE_BG_COLOR = "moreBgColor"; private static LinkedList sValueBuilderPool; public static QMUISkinValueBuilder acquire() { if (sValueBuilderPool == null) { return new QMUISkinValueBuilder(); } QMUISkinValueBuilder valueBuilder = sValueBuilderPool.poll(); if (valueBuilder != null) { return valueBuilder; } return new QMUISkinValueBuilder(); } public static void release(@NonNull QMUISkinValueBuilder valueBuilder) { valueBuilder.clear(); if (sValueBuilderPool == null) { sValueBuilderPool = new LinkedList<>(); } if (sValueBuilderPool.size() < 2) { sValueBuilderPool.push(valueBuilder); } } private QMUISkinValueBuilder() { } private HashMap mValues = new HashMap<>(); public QMUISkinValueBuilder background(int attr) { mValues.put(BACKGROUND, String.valueOf(attr)); return this; } public QMUISkinValueBuilder background(String attrName) { mValues.put(BACKGROUND, attrName); return this; } public QMUISkinValueBuilder underline(int attr) { mValues.put(UNDERLINE, String.valueOf(attr)); return this; } public QMUISkinValueBuilder underline(String attrName) { mValues.put(UNDERLINE, attrName); return this; } public QMUISkinValueBuilder moreTextColor(int attr) { mValues.put(MORE_TEXT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder moreTextColor(String attrName) { mValues.put(MORE_TEXT_COLOR, attrName); return this; } public QMUISkinValueBuilder moreBgColor(int attr) { mValues.put(MORE_BG_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder moreBgColor(String attrName) { mValues.put(MORE_BG_COLOR, attrName); return this; } public QMUISkinValueBuilder textCompoundTintColor(int attr) { mValues.put(TEXT_COMPOUND_TINT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textCompoundTintColor(String attrName) { mValues.put(TEXT_COMPOUND_TINT_COLOR, attrName); return this; } public QMUISkinValueBuilder textCompoundTopSrc(int attr) { mValues.put(TEXT_COMPOUND_TOP_SRC, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textCompoundTopSrc(String attrName) { mValues.put(TEXT_COMPOUND_TOP_SRC, attrName); return this; } public QMUISkinValueBuilder textCompoundRightSrc(int attr) { mValues.put(TEXT_COMPOUND_RIGHT_SRC, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textCompoundRightSrc(String attrName) { mValues.put(TEXT_COMPOUND_RIGHT_SRC, attrName); return this; } public QMUISkinValueBuilder textCompoundBottomSrc(int attr) { mValues.put(TEXT_COMPOUND_BOTTOM_SRC, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textCompoundBottomSrc(String attrName) { mValues.put(TEXT_COMPOUND_BOTTOM_SRC, attrName); return this; } public QMUISkinValueBuilder textCompoundLeftSrc(int attr) { mValues.put(TEXT_COMPOUND_LEFT_SRC, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textCompoundLeftSrc(String attrName) { mValues.put(TEXT_COMPOUND_LEFT_SRC, attrName); return this; } public QMUISkinValueBuilder textColor(int attr) { mValues.put(TEXT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder textColor(String attrName) { mValues.put(TEXT_COLOR, attrName); return this; } public QMUISkinValueBuilder hintColor(int attr) { mValues.put(HINT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder hintColor(String attrName) { mValues.put(HINT_COLOR, attrName); return this; } public QMUISkinValueBuilder progressColor(int attr) { mValues.put(PROGRESS_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder progressColor(String attrName) { mValues.put(PROGRESS_COLOR, attrName); return this; } public QMUISkinValueBuilder src(int attr) { mValues.put(SRC, String.valueOf(attr)); return this; } public QMUISkinValueBuilder src(String attrName) { mValues.put(SRC, attrName); return this; } public QMUISkinValueBuilder border(int attr) { mValues.put(BORDER, String.valueOf(attr)); return this; } public QMUISkinValueBuilder border(String attrName) { mValues.put(BORDER, attrName); return this; } public QMUISkinValueBuilder topSeparator(int attr) { mValues.put(TOP_SEPARATOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder topSeparator(String attrName) { mValues.put(TOP_SEPARATOR, attrName); return this; } public QMUISkinValueBuilder rightSeparator(int attr) { mValues.put(RIGHT_SEPARATOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder rightSeparator(String attrName) { mValues.put(RIGHT_SEPARATOR, attrName); return this; } public QMUISkinValueBuilder bottomSeparator(int attr) { mValues.put(BOTTOM_SEPARATOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder bottomSeparator(String attrName) { mValues.put(BOTTOM_SEPARATOR, attrName); return this; } public QMUISkinValueBuilder leftSeparator(int attr) { mValues.put(LEFT_SEPARATOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder leftSeparator(String attrName) { mValues.put(LEFT_SEPARATOR, attrName); return this; } public QMUISkinValueBuilder alpha(int attr) { mValues.put(ALPHA, String.valueOf(attr)); return this; } public QMUISkinValueBuilder alpha(String attrName) { mValues.put(ALPHA, attrName); return this; } public QMUISkinValueBuilder tintColor(int attr) { mValues.put(TINT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder tintColor(String attrName) { mValues.put(TINT_COLOR, attrName); return this; } public QMUISkinValueBuilder bgTintColor(int attr) { mValues.put(BG_TINT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder bgTintColor(String attrName) { mValues.put(BG_TINT_COLOR, attrName); return this; } public QMUISkinValueBuilder secondTextColor(int attr) { mValues.put(SECOND_TEXT_COLOR, String.valueOf(attr)); return this; } public QMUISkinValueBuilder secondTextColor(String attrName) { mValues.put(SECOND_TEXT_COLOR, attrName); return this; } public QMUISkinValueBuilder custom(String name, int attr) { mValues.put(name, String.valueOf(attr)); return this; } public QMUISkinValueBuilder custom(String name, String attrName) { mValues.put(name, attrName); return this; } public QMUISkinValueBuilder clear() { mValues.clear(); return this; } public QMUISkinValueBuilder convertFrom(String value) { String[] items = value.split("[|]"); for (String item : items) { String[] kv = item.split(":"); if (kv.length != 2) { continue; } mValues.put(kv[0].trim(), kv[1].trim()); } return this; } public boolean isEmpty() { return mValues.isEmpty(); } public String build() { StringBuilder builder = new StringBuilder(); boolean isFirstItem = true; for (String name : mValues.keySet()) { String itemValue = mValues.get(name); if (itemValue == null || itemValue.isEmpty()) { continue; } if (!isFirstItem) { builder.append("|"); } builder.append(name); builder.append(":"); builder.append(itemValue); isFirstItem = false; } return builder.toString(); } public void release() { QMUISkinValueBuilder.release(this); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/SkinWriter.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.skin; public interface SkinWriter { public void write(QMUISkinValueBuilder builder); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/annotation/QMUISkinChangeNotAdapted.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.skin.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface QMUISkinChangeNotAdapted { } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/annotation/QMUISkinListenWithHierarchyChange.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.skin.annotation; 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 QMUISkinListenWithHierarchyChange { } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/defaultAttr/IQMUISkinDefaultAttrProvider.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.skin.defaultAttr; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; public interface IQMUISkinDefaultAttrProvider { @Nullable SimpleArrayMap getDefaultSkinAttrs(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/defaultAttr/QMUISkinSimpleDefaultAttrProvider.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.skin.defaultAttr; import androidx.collection.SimpleArrayMap; public class QMUISkinSimpleDefaultAttrProvider implements IQMUISkinDefaultAttrProvider { private SimpleArrayMap mSkinAttrs = new SimpleArrayMap<>(); public void setDefaultSkinAttr(String name, int attr) { mSkinAttrs.put(name, attr); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return mSkinAttrs; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/IQMUISkinRuleHandler.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.skin.handler; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinManager; public interface IQMUISkinRuleHandler { void handle(@NonNull QMUISkinManager skinManager, @NonNull View view, @NonNull Resources.Theme theme, @NonNull String name, int attr); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleAlphaHandler.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.skin.handler; import android.view.View; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleAlphaHandler extends QMUISkinRuleFloatHandler { @Override protected void handle(@NotNull View view, @NotNull String name, float value) { view.setAlpha(value); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleBackgroundHandler.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.skin.handler; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.view.View; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUIProgressBar; import com.qmuiteam.qmui.widget.QMUISlider; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleBackgroundHandler implements IQMUISkinRuleHandler { @Override public void handle(@NotNull QMUISkinManager skinManager, @NotNull View view, @NotNull Resources.Theme theme, @NotNull String name, int attr) { if(view instanceof QMUIRoundButton){ ((QMUIRoundButton)view).setBgData( QMUIResHelper.getAttrColorStateList(view.getContext(), theme, attr)); }else if(view instanceof QMUIProgressBar){ view.setBackgroundColor(QMUIResHelper.getAttrColor(theme, attr)); }else if(view instanceof QMUISlider){ ((QMUISlider)view).setBarNormalColor(QMUIResHelper.getAttrColor(theme, attr)); }else{ QMUIViewHelper.setBackgroundKeepingPadding(view, QMUIResHelper.getAttrDrawable(view.getContext(), theme, attr)); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleBgTintColorHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import androidx.core.view.TintableBackgroundView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleBgTintColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if (view instanceof TintableBackgroundView) { ((TintableBackgroundView) view).setSupportBackgroundTintList(colorStateList); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleBorderHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import com.qmuiteam.qmui.layout.IQMUILayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView; import com.qmuiteam.qmui.widget.QMUISlider; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleBorderHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if(colorStateList == null){ return; } if (view instanceof IQMUILayout) { ((IQMUILayout) view).setBorderColor(colorStateList.getDefaultColor()); } else if (view instanceof QMUIRadiusImageView) { ((QMUIRadiusImageView) view).setBorderColor(colorStateList.getDefaultColor()); } else if (view instanceof QMUIRoundButton) { ((QMUIRoundButton) view).setStrokeColors(colorStateList); } else if(view instanceof QMUISlider.DefaultThumbView){ ((QMUISlider.DefaultThumbView)view).setBorderColor(colorStateList.getDefaultColor()); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleColorHandler.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.skin.handler; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; public abstract class QMUISkinRuleColorHandler implements IQMUISkinRuleHandler { @Override public final void handle(@NotNull QMUISkinManager skinManager, @NotNull View view, @NotNull Resources.Theme theme, @NotNull String name, int attr) { handle(view, name, QMUIResHelper.getAttrColor(theme, attr)); } protected abstract void handle(@NonNull View view, @NonNull String name, int color); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleColorStateListHandler.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.skin.handler; import android.content.res.ColorStateList; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; public abstract class QMUISkinRuleColorStateListHandler implements IQMUISkinRuleHandler { @Override public final void handle(@NotNull QMUISkinManager skinManager, @NotNull View view, @NotNull Resources.Theme theme, @NotNull String name, int attr) { handle(view, name, QMUIResHelper.getAttrColorStateList(view.getContext(), theme, attr)); } protected abstract void handle(@NonNull View view, @NonNull String name, ColorStateList colorStateList); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleDrawableHandler.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.skin.handler; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.view.View; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; public abstract class QMUISkinRuleDrawableHandler implements IQMUISkinRuleHandler { @Override public final void handle(@NotNull @NonNull QMUISkinManager skinManager, @NotNull @NonNull View view, @NotNull @NonNull Resources.Theme theme, @NotNull @NonNull String name, int attr) { handle(view, name, QMUIResHelper.getAttrDrawable(view.getContext(), theme, attr)); } protected abstract void handle(@NonNull View view, @NonNull String name, Drawable drawable); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleFloatHandler.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.skin.handler; import android.content.res.Resources; import android.view.View; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; public abstract class QMUISkinRuleFloatHandler implements IQMUISkinRuleHandler { @Override public final void handle(@NotNull QMUISkinManager skinManager, @NotNull View view, @NotNull Resources.Theme theme, @NotNull String name, int attr) { handle(view, name, QMUIResHelper.getAttrFloatValue(theme, attr)); } protected abstract void handle(@NonNull View view, @NonNull String name, float value); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleHintColorHandler.java ================================================ package com.qmuiteam.qmui.skin.handler; import android.content.res.ColorStateList; import android.view.View; import android.widget.TextView; import com.google.android.material.textfield.TextInputLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.widget.QMUISlider; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleHintColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if (view instanceof TextView) { ((TextView) view).setHintTextColor(colorStateList); } else if (view instanceof TextInputLayout) { ((TextInputLayout) view).setHintTextColor(colorStateList); }else if(view instanceof QMUISlider){ ((QMUISlider)view).setRecordProgressColor(colorStateList.getDefaultColor()); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleMoreBgColorHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleMoreBgColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if (view instanceof QMUIQQFaceView) { ((QMUIQQFaceView) view).setMoreActionBgColor(colorStateList); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleMoreTextColorHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleMoreTextColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if (view instanceof QMUIQQFaceView) { ((QMUIQQFaceView) view).setMoreActionColor(colorStateList); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleProgressColorHandler.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.skin.handler; import android.view.View; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.widget.QMUIProgressBar; import com.qmuiteam.qmui.widget.QMUISlider; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleProgressColorHandler extends QMUISkinRuleColorHandler { @Override protected void handle(@NotNull View view, @NotNull String name, int color) { if (view instanceof QMUIProgressBar) { ((QMUIProgressBar) view).setProgressColor(color); }else if(view instanceof QMUISlider){ ((QMUISlider) view).setBarProgressColor(color); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleSeparatorHandler.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.skin.handler; import android.view.View; import com.qmuiteam.qmui.layout.IQMUILayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleSeparatorHandler extends QMUISkinRuleColorHandler { @Override protected void handle(@NotNull View view, @NotNull String name, int color) { if (view instanceof IQMUILayout) { if (QMUISkinValueBuilder.TOP_SEPARATOR.equals(name)) { ((IQMUILayout) view).updateTopSeparatorColor(color); } else if (QMUISkinValueBuilder.BOTTOM_SEPARATOR.equals(name)) { ((IQMUILayout) view).updateBottomSeparatorColor(color); } else if (QMUISkinValueBuilder.LEFT_SEPARATOR.equals(name)) { ((IQMUILayout) view).updateLeftSeparatorColor(color); } else if (QMUISkinValueBuilder.RIGHT_SEPARATOR.equals(name)) { ((IQMUILayout) view).updateRightSeparatorColor(color); } }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleSrcHandler.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.skin.handler; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.CompoundButton; import android.widget.ImageView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleSrcHandler extends QMUISkinRuleDrawableHandler { @Override protected void handle(@NotNull View view, @NotNull String name, Drawable drawable) { if (view instanceof ImageView) { ((ImageView) view).setImageDrawable(drawable); } else if (view instanceof CompoundButton) { ((CompoundButton) view).setButtonDrawable(drawable); } else { QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleTextColorHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.widget.QMUIProgressBar; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleTextColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if(colorStateList == null){ return; } if (view instanceof TextView) { ((TextView) view).setTextColor(colorStateList); } else if (view instanceof QMUIQQFaceView) { ((QMUIQQFaceView) view).setTextColor(colorStateList.getDefaultColor()); }else if(view instanceof QMUIProgressBar){ ((QMUIProgressBar) view).setTextColor(colorStateList.getDefaultColor()); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleTextCompoundSrcHandler.java ================================================ package com.qmuiteam.qmui.skin.handler; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleTextCompoundSrcHandler extends QMUISkinRuleDrawableHandler { @Override protected void handle(@NotNull View view, @NotNull String name, Drawable drawable) { if (view instanceof TextView) { TextView tv = (TextView) view; if (drawable != null) { drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } Drawable[] drawables = tv.getCompoundDrawables(); if (QMUISkinValueBuilder.TEXT_COMPOUND_LEFT_SRC.equals(name)) { drawables[0] = drawable; } else if (QMUISkinValueBuilder.TEXT_COMPOUND_TOP_SRC.equals(name)) { drawables[1] = drawable; } else if (QMUISkinValueBuilder.TEXT_COMPOUND_RIGHT_SRC.equals(name)) { drawables[2] = drawable; } else if (QMUISkinValueBuilder.TEXT_COMPOUND_BOTTOM_SRC.equals(name)) { drawables[3] = drawable; } tv.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleTextCompoundTintColorHandler.java ================================================ package com.qmuiteam.qmui.skin.handler; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.util.QMUIDrawableHelper; import androidx.core.widget.TintableCompoundDrawablesView; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleTextCompoundTintColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if(colorStateList == null){ return; } if (view instanceof TextView) { TextView tv = (TextView) view; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { tv.setCompoundDrawableTintList(colorStateList); } else if (tv instanceof TintableCompoundDrawablesView) { ((TintableCompoundDrawablesView) tv).setSupportCompoundDrawablesTintList(colorStateList); } else { Drawable[] drawables = tv.getCompoundDrawables(); for (int i = 0; i < drawables.length; i++) { Drawable drawable = drawables[i]; if (drawable != null) { drawable = drawable.mutate(); QMUIDrawableHelper.setDrawableTintColor(drawable, colorStateList.getDefaultColor()); drawables[i] = drawable; } } tv.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]); } }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleTintColorHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import android.widget.CompoundButton; import android.widget.ImageView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUILoadingView; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout; import androidx.core.widget.CompoundButtonCompat; import androidx.core.widget.ImageViewCompat; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleTintColorHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if(colorStateList == null){ return; } if(view instanceof QMUILoadingView){ ((QMUILoadingView) view).setColor(colorStateList.getDefaultColor()); }else if(view instanceof QMUIPullRefreshLayout.RefreshView){ ((QMUIPullRefreshLayout.RefreshView)view).setColorSchemeColors(colorStateList.getDefaultColor()); }else if (view instanceof ImageView) { ImageViewCompat.setImageTintList((ImageView) view, colorStateList); }else if(view instanceof CompoundButton){ CompoundButtonCompat.setButtonTintList((CompoundButton)view, colorStateList); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/skin/handler/QMUISkinRuleUnderlineHandler.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.skin.handler; import android.content.res.ColorStateList; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.widget.QMUIProgressBar; import org.jetbrains.annotations.NotNull; public class QMUISkinRuleUnderlineHandler extends QMUISkinRuleColorStateListHandler { @Override protected void handle(@NotNull View view, @NotNull String name, ColorStateList colorStateList) { if (view instanceof QMUIQQFaceView) { ((QMUIQQFaceView) view).setLinkUnderLineColor(colorStateList); }else{ QMUISkinHelper.warnRuleNotSupport(view, name); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUIAlignMiddleImageSpan.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.span; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.style.ImageSpan; import android.view.View; import com.qmuiteam.qmui.skin.IQMUISkinHandlerSpan; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIDrawableHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import androidx.annotation.NonNull; import org.jetbrains.annotations.NotNull; /** * 支持垂直居中的ImageSpan * * @author cginechen * @date 2016-03-17 */ public class QMUIAlignMiddleImageSpan extends ImageSpan implements IQMUISkinHandlerSpan { public static final int ALIGN_MIDDLE = -100; // 不要和父类重复 /** * 规定这个Span占几个字的宽度 */ private float mFontWidthMultiple = -1f; /** * 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改 */ private boolean mAvoidSuperChangeFontMetrics = false; @SuppressWarnings("FieldCanBeLocal") private int mWidth; private Drawable mDrawable; private int mDrawableTintColorAttr; /** * @param d 作为 span 的 Drawable * @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE} */ public QMUIAlignMiddleImageSpan(Drawable d, int verticalAlignment) { this(d, verticalAlignment, 0); } /** * @param d 作为 span 的 Drawable * @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE} * @param fontWidthMultiple 设置这个Span占几个中文字的宽度, 当该值 > 0 时, span 的宽度为该值*一个中文字的宽度; 当该值 <= 0 时, span 的宽度由 {@link #mAvoidSuperChangeFontMetrics} 决定 */ public QMUIAlignMiddleImageSpan(@NonNull Drawable d, int verticalAlignment, float fontWidthMultiple) { super(d.mutate(), verticalAlignment); mDrawable = getDrawable(); if (fontWidthMultiple >= 0) { mFontWidthMultiple = fontWidthMultiple; } } public void setSkinSupportWithTintColor(View skinFollowView, int drawableTintColorAttr) { mDrawableTintColorAttr = drawableTintColorAttr; if (mDrawable != null && skinFollowView != null && drawableTintColorAttr != 0) { QMUIDrawableHelper.setDrawableTintColor(mDrawable, QMUISkinHelper.getSkinColor(skinFollowView, drawableTintColorAttr)); skinFollowView.invalidate(); } } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { if (mAvoidSuperChangeFontMetrics) { Drawable d = getDrawable(); Rect rect = d.getBounds(); mWidth = rect.right; } else { mWidth = super.getSize(paint, text, start, end, fm); } if (mFontWidthMultiple > 0) { mWidth = (int) (paint.measureText("子") * mFontWidthMultiple); } return mWidth; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { if (mVerticalAlignment == ALIGN_MIDDLE) { Drawable d = mDrawable; canvas.save(); // // 注意如果这样实现会有问题:TextView 有 lineSpacing 时,这里 bottom 偏大,导致偏下 // int transY = bottom - d.getBounds().bottom; // 底对齐 // transY -= (paint.getFontMetricsInt().bottom - paint.getFontMetricsInt().top) / 2 - d.getBounds().bottom / 2; // 居中对齐 // canvas.translate(x, transY); // d.draw(canvas); // canvas.restore(); Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); int fontTop = y + fontMetricsInt.top; int fontMetricsHeight = fontMetricsInt.bottom - fontMetricsInt.top; int iconHeight = d.getBounds().bottom - d.getBounds().top; int iconTop = fontTop + (fontMetricsHeight - iconHeight) / 2; canvas.translate(x, iconTop); d.draw(canvas); canvas.restore(); } else { super.draw(canvas, text, start, end, x, top, y, bottom, paint); } } /** * 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改 */ public void setAvoidSuperChangeFontMetrics(boolean avoidSuperChangeFontMetrics) { mAvoidSuperChangeFontMetrics = avoidSuperChangeFontMetrics; } @Override public void handle(@NotNull View view, @NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme) { if (mDrawableTintColorAttr != 0) { QMUIDrawableHelper.setDrawableTintColor(mDrawable, QMUIResHelper.getAttrColor(theme, mDrawableTintColorAttr)); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUIBlockSpaceSpan.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.span; import android.graphics.Canvas; import android.graphics.Paint; import androidx.annotation.NonNull; import android.text.style.ReplacementSpan; import com.qmuiteam.qmui.util.QMUIDeviceHelper; /** * 提供一个整行的空白的Span,可用来用于制作段间距 * * @author cginechen * @date 2016-02-17 */ public class QMUIBlockSpaceSpan extends ReplacementSpan { private int mHeight; public QMUIBlockSpaceSpan(int height) { mHeight = height; } @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { if (fm != null && !QMUIDeviceHelper.isMeizu()) { //return后宽度为0,因此实际空隙和段落开始在同一行,需要加上一行的高度 fm.ascent = fm.top = -mHeight - paint.getFontMetricsInt(fm); fm.descent = fm.bottom = 0; } return 0; } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUICustomTypefaceSpan.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.span; import android.graphics.Paint; import android.graphics.Typeface; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import android.text.TextPaint; import android.text.style.TypefaceSpan; /** * 支持以 Typeface 的方式设置 span 的字体,实现自定义字体的效果 */ public class QMUICustomTypefaceSpan extends TypefaceSpan { /* http://stackoverflow.com/questions/6612316/how-set-spannable-object-font-with-custom-font#answer-10741161 */ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public QMUICustomTypefaceSpan createFromParcel(Parcel source) { return null; } @Override public QMUICustomTypefaceSpan[] newArray(int size) { return new QMUICustomTypefaceSpan[size]; } }; private final @Nullable Typeface newType; /** * @param family Typeface 字体的字体名 * @param type 该字体的 Typeface 对象 */ public QMUICustomTypefaceSpan(String family, @Nullable Typeface type) { super(family); newType = type; } private static void applyCustomTypeFace(Paint paint, @Nullable Typeface tf) { if (tf == null) { return; } int oldStyle; Typeface old = paint.getTypeface(); if (old == null) { oldStyle = Typeface.NORMAL; } else { oldStyle = old.getStyle(); } int fake = oldStyle & ~tf.getStyle(); if ((fake & Typeface.BOLD) != 0) { paint.setFakeBoldText(true); } if ((fake & Typeface.ITALIC) != 0) { paint.setTextSkewX(-0.25f); } paint.setTypeface(tf); } @Override public void updateDrawState(TextPaint ds) { applyCustomTypeFace(ds, newType); } @Override public void updateMeasureState(TextPaint paint) { applyCustomTypeFace(paint, newType); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUIMarginImageSpan.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.span; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.view.View; /** * 支持设置图片左右间距的 ImageSpan * * @author chantchen * @date 2015-12-16 */ public class QMUIMarginImageSpan extends QMUIAlignMiddleImageSpan { private int mSpanMarginLeft = 0; private int mSpanMarginRight = 0; private int mOffsetY = 0; public QMUIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight) { this(d, verticalAlignment, marginLeft, marginRight, 0); } public QMUIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight, int offsetY) { super(d, verticalAlignment); mSpanMarginLeft = marginLeft; mSpanMarginRight = marginRight; mOffsetY = offsetY; } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { if (mSpanMarginLeft != 0 || mSpanMarginRight != 0) { super.getSize(paint, text, start, end, fm); Drawable d = getDrawable(); return d.getIntrinsicWidth() + mSpanMarginLeft + mSpanMarginRight; } else { return super.getSize(paint, text, start, end, fm); } } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { canvas.save(); canvas.translate(0, mOffsetY); // marginRight不用专门处理,只靠getSize()中改变即可 super.draw(canvas, text, start, end, x + mSpanMarginLeft, top, y, bottom, paint); canvas.restore(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUIOnSpanClickListener.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.span; /** * @author cginechen * @date 2017-03-20 */ public interface QMUIOnSpanClickListener { boolean onSpanClick(String text); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUITextSizeSpan.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.span; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.text.style.ReplacementSpan; import androidx.annotation.NonNull; /** * 支持调整字体大小的 span。{@link android.text.style.AbsoluteSizeSpan} 可以调整字体大小,但在中英文混排下由于 decent 的不同, * 无法根据具体需求进行底部对齐或者顶部对齐。而 QMUITextSizeSpan 则可以多传一个参数,让你可以根据具体情况来决定偏移值。 * * @author cginechen * @date 2016-12-02 */ public class QMUITextSizeSpan extends ReplacementSpan { private int mTextSize; private int mVerticalOffset; private Paint mPaint; private Typeface mTypeface; public QMUITextSizeSpan(int textSize, int verticalOffset){ this(textSize, verticalOffset, null); } public QMUITextSizeSpan(int textSize, int verticalOffset, Typeface typeface){ mTextSize = textSize; mVerticalOffset = verticalOffset; mTypeface = typeface; mPaint = new Paint(); mPaint.setTextSize(mTextSize); mPaint.setTypeface(mTypeface); } @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { if(mTextSize > paint.getTextSize() && fm != null){ Paint.FontMetricsInt newFm = mPaint.getFontMetricsInt(); fm.descent = newFm.descent; fm.ascent = newFm.ascent; fm.top = newFm.top; fm.bottom = newFm.bottom; } return (int) mPaint.measureText(text, start, end); } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { mPaint.setColor(paint.getColor()); mPaint.setStyle(paint.getStyle()); mPaint.setAntiAlias(paint.isAntiAlias()); int baseline = y + mVerticalOffset; canvas.drawText(text, start, end, x, baseline, mPaint); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/span/QMUITouchableSpan.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.span; import android.content.res.Resources; import android.text.TextPaint; import android.text.style.ClickableSpan; import android.view.View; import com.qmuiteam.qmui.QMUILog; import com.qmuiteam.qmui.link.ITouchableSpan; import com.qmuiteam.qmui.skin.IQMUISkinHandlerSpan; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import androidx.annotation.ColorInt; import androidx.core.view.ViewCompat; import org.jetbrains.annotations.NotNull; /** * 可 Touch 的 Span,在 {@link #setPressed(boolean)} 后根据是否 pressed 来触发不同的UI状态 *

* 提供设置 span 的文字颜色和背景颜色的功能, 在构造时传入 *

*/ public abstract class QMUITouchableSpan extends ClickableSpan implements ITouchableSpan, IQMUISkinHandlerSpan { private static final String TAG = "QMUITouchableSpan"; private boolean mIsPressed; @ColorInt private int mNormalBackgroundColor; @ColorInt private int mPressedBackgroundColor; @ColorInt private int mNormalTextColor; @ColorInt private int mPressedTextColor; private int mNormalBgAttr; private int mPressedBgAttr; private int mNormalTextColorAttr; private int mPressedTextColorAttr; private boolean mIsNeedUnderline = false; public abstract void onSpanClick(View widget); @Override public final void onClick(View widget) { if (ViewCompat.isAttachedToWindow(widget)) { onSpanClick(widget); } } public QMUITouchableSpan(@ColorInt int normalTextColor, @ColorInt int pressedTextColor, @ColorInt int normalBackgroundColor, @ColorInt int pressedBackgroundColor) { mNormalTextColor = normalTextColor; mPressedTextColor = pressedTextColor; mNormalBackgroundColor = normalBackgroundColor; mPressedBackgroundColor = pressedBackgroundColor; } public QMUITouchableSpan(View initFollowSkinView, int normalTextColorAttr, int pressedTextColorAttr, int normalBgAttr, int pressedBgAttr) { mNormalBgAttr = normalBgAttr; mPressedBgAttr = pressedBgAttr; mNormalTextColorAttr = normalTextColorAttr; mPressedTextColorAttr = pressedTextColorAttr; if (normalTextColorAttr != 0) { mNormalTextColor = QMUISkinHelper.getSkinColor(initFollowSkinView, normalTextColorAttr); } if (pressedTextColorAttr != 0) { mPressedTextColor = QMUISkinHelper.getSkinColor(initFollowSkinView, pressedTextColorAttr); } if (normalBgAttr != 0) { mNormalBackgroundColor = QMUISkinHelper.getSkinColor(initFollowSkinView, normalBgAttr); } if (pressedBgAttr != 0) { mPressedBackgroundColor = QMUISkinHelper.getSkinColor(initFollowSkinView, pressedBgAttr); } } public int getNormalBackgroundColor() { return mNormalBackgroundColor; } public void setNormalTextColor(int normalTextColor) { mNormalTextColor = normalTextColor; } public void setPressedTextColor(int pressedTextColor) { mPressedTextColor = pressedTextColor; } public int getNormalTextColor() { return mNormalTextColor; } public int getPressedBackgroundColor() { return mPressedBackgroundColor; } public int getPressedTextColor() { return mPressedTextColor; } public void setPressed(boolean isSelected) { mIsPressed = isSelected; } public boolean isPressed() { return mIsPressed; } public void setIsNeedUnderline(boolean isNeedUnderline) { mIsNeedUnderline = isNeedUnderline; } public boolean isNeedUnderline() { return mIsNeedUnderline; } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mIsPressed ? mPressedTextColor : mNormalTextColor); ds.bgColor = mIsPressed ? mPressedBackgroundColor : mNormalBackgroundColor; ds.setUnderlineText(mIsNeedUnderline); } @Override public void handle(@NotNull View view, @NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme) { boolean noAttrExist = true; if (mNormalTextColorAttr != 0) { mNormalTextColor = QMUIResHelper.getAttrColor(theme, mNormalTextColorAttr); noAttrExist = false; } if (mPressedTextColorAttr != 0) { mPressedTextColor = QMUIResHelper.getAttrColor(theme, mPressedTextColorAttr); noAttrExist = false; } if (mNormalBgAttr != 0) { mNormalBackgroundColor = QMUIResHelper.getAttrColor(theme, mNormalBgAttr); noAttrExist = false; } if (mPressedBgAttr != 0) { mPressedBackgroundColor = QMUIResHelper.getAttrColor(theme, mPressedBgAttr); noAttrExist = false; } if (noAttrExist) { QMUILog.w(TAG, "There are no attrs for skin. Please use constructor with 5 parameters"); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/OnceReadValue.java ================================================ package com.qmuiteam.qmui.util; public abstract class OnceReadValue { private volatile boolean isRead = false; private T cacheValue; public T get(P param){ if(isRead){ return cacheValue; } synchronized (this){ if(!isRead){ cacheValue = read(param); isRead = true; } } return cacheValue; } protected abstract T read(P param); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIActivityLifecycleCallbacks.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.util; import android.app.Activity; import android.app.Application; import android.os.Bundle; /** * @author cginechen * @date 2016-11-07 * * https://github.com/yshrsmz/KeyboardVisibilityEvent/blob/master/keyboardvisibilityevent/src/main/java/net/yslibrary/android/keyboardvisibilityevent/AutoActivityLifecycleCallback.java */ public abstract class QMUIActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { private final Activity mTargetActivity; public QMUIActivityLifecycleCallbacks(Activity targetActivity) { mTargetActivity = targetActivity; } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { if (activity == mTargetActivity) { mTargetActivity.getApplication().unregisterActivityLifecycleCallbacks(this); onTargetActivityDestroyed(); } } protected abstract void onTargetActivityDestroyed(); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUICollapsingTextHelper.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. */ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qmuiteam.qmui.util; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Build; import android.text.TextPaint; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.ColorInt; import androidx.annotation.RequiresApi; import androidx.core.text.TextDirectionHeuristicsCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.R; public final class QMUICollapsingTextHelper { // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it // by using our own texture private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18; private static final boolean DEBUG_DRAW = false; private static final Paint DEBUG_DRAW_PAINT; static { // 测试逻辑,不作检测 // noinspection ConstantConditions DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null; // noinspection ConstantConditions if (DEBUG_DRAW_PAINT != null) { DEBUG_DRAW_PAINT.setAntiAlias(true); DEBUG_DRAW_PAINT.setColor(Color.MAGENTA); } } private final View mView; private boolean mDrawTitle; private float mExpandedFraction; private final Rect mExpandedBounds; private final Rect mCollapsedBounds; private final RectF mCurrentBounds; private int mExpandedTextGravity = Gravity.CENTER_VERTICAL; private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL; private float mExpandedTextSize = 15; private float mCollapsedTextSize = 15; private ColorStateList mExpandedTextColor; private ColorStateList mCollapsedTextColor; private float mExpandedDrawY; private float mCollapsedDrawY; private float mExpandedDrawX; private float mCollapsedDrawX; private float mCurrentDrawX; private float mCurrentDrawY; private float mCollapsedTextWidth; private float mExpandedTextWidth; private float mCurrentTextWidth; private float mCollapsedTextHeight; private float mExpandedTextHeight; private float mCurrentTextHeight; private Typeface mCollapsedTypeface; private Typeface mExpandedTypeface; private Typeface mCurrentTypeface; private float mTypefaceUpdateAreaPercent; private CharSequence mText; private CharSequence mTextToDraw; private boolean mIsRtl; private boolean mUseTexture; private Bitmap mExpandedTitleTexture; private Paint mTexturePaint; private float mTextureAscent; private float mTextureDescent; private float mScale; private float mCurrentTextSize; private int[] mState; private boolean mBoundsChanged; private final TextPaint mTextPaint; private Interpolator mPositionInterpolator; private Interpolator mTextSizeInterpolator; private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy; private int mCollapsedShadowColor; private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy; private int mExpandedShadowColor; public QMUICollapsingTextHelper(View view){ this(view, 0f); } public QMUICollapsingTextHelper(View view, float defaultExpanededFraction) { mView = view; mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); mExpandedFraction = defaultExpanededFraction; mCollapsedBounds = new Rect(); mExpandedBounds = new Rect(); mCurrentBounds = new RectF(); } public void setTextSizeInterpolator(Interpolator interpolator) { mTextSizeInterpolator = interpolator; recalculate(); } public void setPositionInterpolator(Interpolator interpolator) { mPositionInterpolator = interpolator; recalculate(); } public void setTextSize(float collapsedTextSize, float expandedTextSize, boolean recalculate){ if(mExpandedTextSize != expandedTextSize || mCollapsedTextSize != collapsedTextSize){ mExpandedTextSize = expandedTextSize; mCollapsedTextSize = collapsedTextSize; if(recalculate){ recalculate(); } } } public void setExpandedTextSize(float textSize) { if (mExpandedTextSize != textSize) { mExpandedTextSize = textSize; recalculate(); } } public void setCollapsedTextSize(float textSize) { if (mCollapsedTextSize != textSize) { mCollapsedTextSize = textSize; recalculate(); } } public void setCollapsedTextColor(ColorStateList textColor) { if (mCollapsedTextColor != textColor) { mCollapsedTextColor = textColor; recalculate(); } } public void setExpandedTextColor(ColorStateList textColor) { if (mExpandedTextColor != textColor) { mExpandedTextColor = textColor; recalculate(); } } public void setTextColor(ColorStateList collapsedTextColor, ColorStateList expandedTextColor, boolean recalculate){ if(mCollapsedTextColor != collapsedTextColor || mExpandedTextColor != expandedTextColor){ mCollapsedTextColor = collapsedTextColor; mExpandedTextColor = expandedTextColor; if(recalculate){ recalculate(); } } } public void setCollapsedTextAppearance(int resId) { TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.QMUITextAppearance); if (a.hasValue(R.styleable.QMUITextAppearance_android_textColor)) { mCollapsedTextColor = a.getColorStateList(R.styleable.QMUITextAppearance_android_textColor); } if (a.hasValue(R.styleable.QMUITextAppearance_android_textSize)) { mCollapsedTextSize = a.getDimensionPixelSize(R.styleable.QMUITextAppearance_android_textSize, (int) mCollapsedTextSize); } mCollapsedShadowColor = a.getInt(R.styleable.QMUITextAppearance_android_shadowColor, 0); mCollapsedShadowDx = a.getFloat(R.styleable.QMUITextAppearance_android_shadowDx, 0); mCollapsedShadowDy = a.getFloat(R.styleable.QMUITextAppearance_android_shadowDy, 0); mCollapsedShadowRadius = a.getFloat(R.styleable.QMUITextAppearance_android_shadowRadius, 0); a.recycle(); mCollapsedTypeface = readFontFamilyTypeface(resId); recalculate(); } public void setExpandedTextAppearance(int resId) { TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.QMUITextAppearance); if (a.hasValue(R.styleable.QMUITextAppearance_android_textColor)) { mExpandedTextColor = a.getColorStateList(R.styleable.QMUITextAppearance_android_textColor); } if (a.hasValue(R.styleable.QMUITextAppearance_android_textSize)) { mExpandedTextSize = a.getDimensionPixelSize(R.styleable.QMUITextAppearance_android_textSize, (int) mExpandedTextSize); } mExpandedShadowColor = a.getInt( R.styleable.QMUITextAppearance_android_shadowColor, 0); mExpandedShadowDx = a.getFloat( R.styleable.QMUITextAppearance_android_shadowDx, 0); mExpandedShadowDy = a.getFloat( R.styleable.QMUITextAppearance_android_shadowDy, 0); mExpandedShadowRadius = a.getFloat( R.styleable.QMUITextAppearance_android_shadowRadius, 0); a.recycle(); mExpandedTypeface = readFontFamilyTypeface(resId); recalculate(); } public void setExpandedBounds(int left, int top, int right, int bottom) { if (!rectEquals(mExpandedBounds, left, top, right, bottom)) { mExpandedBounds.set(left, top, right, bottom); mBoundsChanged = true; onBoundsChanged(); } } public void setCollapsedBounds(int left, int top, int right, int bottom) { if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) { mCollapsedBounds.set(left, top, right, bottom); mBoundsChanged = true; onBoundsChanged(); } } void onBoundsChanged() { mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0 && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0; } public void setExpandedTextGravity(int gravity) { if (mExpandedTextGravity != gravity) { mExpandedTextGravity = gravity; recalculate(); } } public int getExpandedTextGravity() { return mExpandedTextGravity; } public void setCollapsedTextGravity(int gravity) { if (mCollapsedTextGravity != gravity) { mCollapsedTextGravity = gravity; recalculate(); } } public int getCollapsedTextGravity() { return mCollapsedTextGravity; } public void setGravity(int collapsedGravity, int expandedGravity, boolean recalculate){ if(mCollapsedTextGravity != collapsedGravity || mExpandedTextGravity != expandedGravity){ mCollapsedTextGravity = collapsedGravity; mExpandedTextGravity = expandedGravity; if(recalculate){ recalculate(); } } } @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) private Typeface readFontFamilyTypeface(int resId) { final TypedArray a = mView.getContext().obtainStyledAttributes(resId, new int[]{android.R.attr.fontFamily}); try { final String family = a.getString(0); if (family != null) { return Typeface.create(family, Typeface.NORMAL); } } finally { a.recycle(); } return null; } public void setTypeface(Typeface collapsedTypeface, Typeface expandedTypeface, boolean recalculate){ if(mCollapsedTypeface != collapsedTypeface || mExpandedTypeface != expandedTypeface){ mCollapsedTypeface = collapsedTypeface; mExpandedTypeface = expandedTypeface; if(recalculate){ recalculate(); } } } public void setCollapsedTypeface(Typeface typeface) { if (mCollapsedTypeface != typeface) { mCollapsedTypeface = typeface; recalculate(); } } public void setExpandedTypeface(Typeface typeface) { if (mExpandedTypeface != typeface) { mExpandedTypeface = typeface; recalculate(); } } public void setTypefaces(Typeface typeface) { mCollapsedTypeface = mExpandedTypeface = typeface; recalculate(); } public Typeface getCollapsedTypeface() { return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT; } public Typeface getExpandedTypeface() { return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT; } /** * Set the value indicating the current scroll value. This decides how much of the * background will be displayed, as well as the title metrics/positioning. *

* A value of {@code 0.0} indicates that the layout is fully expanded. * A value of {@code 1.0} indicates that the layout is fully collapsed. */ public void setExpansionFraction(float fraction) { fraction = QMUILangHelper.constrain(fraction, 0f, 1f); if (fraction != mExpandedFraction) { mExpandedFraction = fraction; calculateCurrentOffsets(); } } public final boolean setState(final int[] state) { mState = state; if (isStateful()) { recalculate(); return true; } return false; } public void setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { mTypefaceUpdateAreaPercent = typefaceUpdateAreaPercent; } public final boolean isStateful() { return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful()) || (mExpandedTextColor != null && mExpandedTextColor.isStateful()); } public float getExpansionFraction() { return mExpandedFraction; } public float getCollapsedTextSize() { return mCollapsedTextSize; } public float getExpandedTextSize() { return mExpandedTextSize; } public void calculateCurrentOffsets() { calculateOffsets(mExpandedFraction); } private void calculateOffsets(final float fraction) { interpolateBounds(fraction); mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, mPositionInterpolator); mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator); mCurrentTextHeight = lerp(mExpandedTextHeight, mCollapsedTextHeight, fraction, mPositionInterpolator); mCurrentTextWidth = lerp(mExpandedTextWidth, mCollapsedTextWidth, fraction, mPositionInterpolator); setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, fraction, mTextSizeInterpolator)); if (mCollapsedTextColor != mExpandedTextColor) { // If the collapsed and expanded text colors are different, blend them based on the // fraction mTextPaint.setColor(QMUIColorHelper.computeColor( getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction)); } else { mTextPaint.setColor(getCurrentCollapsedTextColor()); } mTextPaint.setShadowLayer( lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), QMUIColorHelper.computeColor(mExpandedShadowColor, mCollapsedShadowColor, fraction)); ViewCompat.postInvalidateOnAnimation(mView); } @ColorInt private int getCurrentExpandedTextColor() { if(mExpandedTextColor == null){ return 0; } if (mState != null) { return mExpandedTextColor.getColorForState(mState, 0); } else { return mExpandedTextColor.getDefaultColor(); } } @ColorInt private int getCurrentCollapsedTextColor() { if(mCollapsedTextColor == null){ return 0; } if (mState != null) { return mCollapsedTextColor.getColorForState(mState, 0); } else { return mCollapsedTextColor.getDefaultColor(); } } public void calculateBaseOffsets() { final float currentTextSize = mCurrentTextSize; // We then calculate the collapsed text size, using the same logic calculateUsingTextSize(mCollapsedTextSize); mCollapsedTextWidth = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; mCollapsedTextHeight = mTextPaint.descent() - mTextPaint.ascent(); final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: mCollapsedDrawY = mCollapsedBounds.bottom - mTextPaint.descent(); break; case Gravity.TOP: mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent(); break; case Gravity.CENTER_VERTICAL: default: float textOffset = (mCollapsedTextHeight / 2) - mTextPaint.descent(); mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset; break; } switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: mCollapsedDrawX = mCollapsedBounds.centerX() - (mCollapsedTextWidth / 2); break; case Gravity.RIGHT: mCollapsedDrawX = mCollapsedBounds.right - mCollapsedTextWidth; break; case Gravity.LEFT: default: mCollapsedDrawX = mCollapsedBounds.left; break; } calculateUsingTextSize(mExpandedTextSize); mExpandedTextWidth = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0; mExpandedTextHeight = mTextPaint.descent() - mTextPaint.ascent(); final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: mExpandedDrawY = mExpandedBounds.bottom - mTextPaint.descent(); break; case Gravity.TOP: mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent(); break; case Gravity.CENTER_VERTICAL: default: float textOffset = (mExpandedTextHeight / 2) - mTextPaint.descent(); mExpandedDrawY = mExpandedBounds.centerY() + textOffset; break; } switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: mExpandedDrawX = mExpandedBounds.centerX() - (mExpandedTextWidth / 2); break; case Gravity.RIGHT: mExpandedDrawX = mExpandedBounds.right - mExpandedTextWidth; break; case Gravity.LEFT: default: mExpandedDrawX = mExpandedBounds.left; break; } // The bounds have changed so we need to clear the texture clearTexture(); // Now reset the text size back to the original setInterpolatedTextSize(currentTextSize); } private void interpolateBounds(float fraction) { mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, fraction, mPositionInterpolator); mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator); mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, fraction, mPositionInterpolator); mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, fraction, mPositionInterpolator); } // 系统类原有代码,不作检测 @SuppressWarnings("UnusedAssignment") public void draw(Canvas canvas) { final int saveCount = canvas.save(); if (mTextToDraw != null && mDrawTitle) { float x = mCurrentDrawX; float y = mCurrentDrawY; final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null; final float ascent; final float descent; if (drawTexture) { ascent = mTextureAscent * mScale; descent = mTextureDescent * mScale; } else { ascent = mTextPaint.ascent() * mScale; descent = mTextPaint.descent() * mScale; } if (DEBUG_DRAW) { // Just a debug tool, which drawn a magenta rect in the text bounds canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent, DEBUG_DRAW_PAINT); } if (drawTexture) { y += ascent; } if (mScale != 1f) { canvas.scale(mScale, mScale, x, y); } if (drawTexture) { // If we should use a texture, draw it instead of text canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint); } else { canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint); } } canvas.restoreToCount(saveCount); } private boolean calculateIsRtl(CharSequence text) { final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) == ViewCompat.LAYOUT_DIRECTION_RTL; return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length()); } private void setInterpolatedTextSize(float textSize) { calculateUsingTextSize(textSize); // Use our texture if the scale isn't 1.0 mUseTexture = USE_SCALING_TEXTURE && mScale != 1f; if (mUseTexture) { // Make sure we have an expanded texture if needed ensureExpandedTexture(); } ViewCompat.postInvalidateOnAnimation(mView); } private void calculateUsingTextSize(final float textSize) { if (mText == null) return; final float collapsedWidth = mCollapsedBounds.width(); final float expandedWidth = mExpandedBounds.width(); final float availableWidth; final float newTextSize; boolean updateDrawText = false; if(mExpandedFraction >= 1f - mTypefaceUpdateAreaPercent){ if (mCurrentTypeface != mCollapsedTypeface) { mCurrentTypeface = mCollapsedTypeface; updateDrawText = true; } }else if(mExpandedFraction <= mTypefaceUpdateAreaPercent){ if (mCurrentTypeface != mExpandedTypeface) { mCurrentTypeface = mExpandedTypeface; updateDrawText = true; } } if (isClose(textSize, mCollapsedTextSize)) { newTextSize = mCollapsedTextSize; mScale = 1f; availableWidth = collapsedWidth; } else { newTextSize = mExpandedTextSize; if (isClose(textSize, mExpandedTextSize)) { // If we're close to the expanded text size, snap to it and use a scale of 1 mScale = 1f; } else { // Else, we'll scale down from the expanded text size mScale = textSize / mExpandedTextSize; } final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize; // This is the size of the expanded bounds when it is scaled to match the // collapsed text size final float scaledDownWidth = expandedWidth * textSizeRatio; if (scaledDownWidth > collapsedWidth) { // If the scaled down size is larger than the actual collapsed width, we need to // cap the available width so that when the expanded text scales down, it matches // the collapsed width availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth); } else { // Otherwise we'll just use the expanded width availableWidth = expandedWidth; } } if (availableWidth > 0) { updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText; mCurrentTextSize = newTextSize; mBoundsChanged = false; } if (mTextToDraw == null || updateDrawText) { mTextPaint.setTextSize(mCurrentTextSize); mTextPaint.setTypeface(mCurrentTypeface); // Use linear text scaling if we're scaling the canvas mTextPaint.setLinearText(mScale != 1f); // If we don't currently have text to draw, or the text size has changed, ellipsize... final CharSequence title = TextUtils.ellipsize(mText, mTextPaint, availableWidth, TextUtils.TruncateAt.END); if (!TextUtils.equals(title, mTextToDraw)) { mTextToDraw = title; mIsRtl = calculateIsRtl(mTextToDraw); } } } private void ensureExpandedTexture() { if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() || TextUtils.isEmpty(mTextToDraw)) { return; } calculateOffsets(0f); mTextureAscent = mTextPaint.ascent(); mTextureDescent = mTextPaint.descent(); final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length())); final int h = Math.round(mTextureDescent - mTextureAscent); if (w <= 0 || h <= 0) { return; // If the width or height are 0, return } mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(mExpandedTitleTexture); c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint); if (mTexturePaint == null) { // Make sure we have a paint mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); } } public void recalculate() { if (mView.getHeight() > 0 && mView.getWidth() > 0) { // If we've already been laid out, calculate everything now otherwise we'll wait // until a layout calculateBaseOffsets(); calculateCurrentOffsets(); } } /** * Set the title to display * * @param text content of title */ public void setText(CharSequence text) { if (text == null || !text.equals(mText)) { mText = text; mTextToDraw = null; clearTexture(); recalculate(); } } public CharSequence getText() { return mText; } private void clearTexture() { if (mExpandedTitleTexture != null) { mExpandedTitleTexture.recycle(); mExpandedTitleTexture = null; } } public float getExpandedTextWidth() { return mExpandedTextWidth; } public float getCollapsedTextWidth() { return mCollapsedTextWidth; } public float getExpandedTextHeight() { return mExpandedTextHeight; } public float getCollapsedTextHeight() { return mCollapsedTextHeight; } public float getExpandedDrawX() { return mExpandedDrawX; } public float getCollapsedDrawX() { return mCollapsedDrawX; } /** * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently * defined as it's difference being < 0.001. */ private static boolean isClose(float value, float targetValue) { return Math.abs(value - targetValue) < 0.001f; } ColorStateList getExpandedTextColor() { return mExpandedTextColor; } ColorStateList getCollapsedTextColor() { return mCollapsedTextColor; } public static float lerp(float startValue, float endValue, float fraction, Interpolator interpolator) { if (interpolator != null) { fraction = interpolator.getInterpolation(fraction); } return startValue + Math.round(fraction * (endValue - startValue)); } private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) { return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIColorHelper.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.util; import android.graphics.Color; import androidx.annotation.ColorInt; /** * @author cginechen * @date 2016-03-17 */ public class QMUIColorHelper { public static int setColorAlpha(@ColorInt int color, float alpha) { return setColorAlpha(color, alpha, true); } /** * 设置颜色的alpha值 * * @param color 需要被设置的颜色值 * @param alpha 取值为[0,1],0表示全透明,1表示不透明 * @param override 覆盖原本的 alpha * @return 返回改变了 alpha 值的颜色值 */ public static int setColorAlpha(@ColorInt int color, float alpha, boolean override) { int origin = override ? 0xff : (color >> 24) & 0xff; return color & 0x00ffffff | (int) (alpha * origin) << 24; } /** * 根据比例,在两个color值之间计算出一个color值 * 注意该方法是ARGB通道分开计算比例的 * * @param fromColor 开始的color值 * @param toColor 最终的color值 * @param fraction 比例,取值为[0,1],为0时返回 fromColor, 为1时返回 toColor * @return 计算出的color值 */ public static int computeColor(@ColorInt int fromColor, @ColorInt int toColor, float fraction) { fraction = QMUILangHelper.constrain(fraction, 0f, 1f); int minColorA = Color.alpha(fromColor); int maxColorA = Color.alpha(toColor); int resultA = (int) ((maxColorA - minColorA) * fraction) + minColorA; int minColorR = Color.red(fromColor); int maxColorR = Color.red(toColor); int resultR = (int) ((maxColorR - minColorR) * fraction) + minColorR; int minColorG = Color.green(fromColor); int maxColorG = Color.green(toColor); int resultG = (int) ((maxColorG - minColorG) * fraction) + minColorG; int minColorB = Color.blue(fromColor); int maxColorB = Color.blue(toColor); int resultB = (int) ((maxColorB - minColorB) * fraction) + minColorB; return Color.argb(resultA, resultR, resultG, resultB); } /** * 将 color 颜色值转换为十六进制字符串 * * @param color 颜色值 * @return 转换后的字符串 */ public static String colorToString(@ColorInt int color) { return String.format("#%08X", color); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDeviceHelper.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.util; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.AppOpsManager; import android.content.Context; import android.content.res.Configuration; import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.provider.Settings; import android.text.TextUtils; import androidx.annotation.Nullable; import com.qmuiteam.qmui.QMUILog; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author cginechen * @date 2016-08-11 */ @SuppressLint("PrivateApi") public class QMUIDeviceHelper { private final static String TAG = "QMUIDeviceHelper"; private final static String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name"; private static final String KEY_FLYME_VERSION_NAME = "ro.build.display.id"; private final static String FLYME = "flyme"; private final static String ZTEC2016 = "zte c2016"; private final static String ZUKZ1 = "zuk z1"; private final static String MEIZUBOARD[] = {"m9", "M9", "mx", "MX"}; private final static String POWER_PROFILE_CLASS = "com.android.internal.os.PowerProfile"; private final static String CPU_FILE_PATH_0 = "/sys/devices/system/cpu/"; private final static String CPU_FILE_PATH_1 = "/sys/devices/system/cpu/possible"; private final static String CPU_FILE_PATH_2 = "/sys/devices/system/cpu/present"; private static FileFilter CPU_FILTER = new FileFilter() { @Override public boolean accept(File pathname) { return Pattern.matches("cpu[0-9]", pathname.getName()); } }; private static String sMiuiVersionName; private static String sFlymeVersionName; private static boolean sIsTabletChecked = false; private static boolean sIsTabletValue = false; private static final String BRAND = Build.BRAND.toLowerCase(); private static long sTotalMemory = -1; private static long sInnerStorageSize = -1; private static long sExtraStorageSize = -1; private static double sBatteryCapacity = -1; private static int sCpuCoreCount = -1; private static boolean isInfoReaded = false; private static void checkReadInfo(){ if(isInfoReaded){ return; } isInfoReaded = true; Properties properties = new Properties(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // android 8.0,读取 /system/uild.prop 会报 permission denied FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(new File(Environment.getRootDirectory(), "build.prop")); properties.load(fileInputStream); } catch (Exception e) { QMUILog.printErrStackTrace(TAG, e, "read file error"); } finally { QMUILangHelper.close(fileInputStream); } } Class clzSystemProperties = null; try { clzSystemProperties = Class.forName("android.os.SystemProperties"); Method getMethod = clzSystemProperties.getDeclaredMethod("get", String.class); // miui sMiuiVersionName = getLowerCaseName(properties, getMethod, KEY_MIUI_VERSION_NAME); //flyme sFlymeVersionName = getLowerCaseName(properties, getMethod, KEY_FLYME_VERSION_NAME); } catch (Exception e) { QMUILog.printErrStackTrace(TAG, e, "read SystemProperties error"); } } private static boolean _isTablet(Context context) { return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; } /** * 判断是否为平板设备 */ public static boolean isTablet(Context context) { if (sIsTabletChecked) { return sIsTabletValue; } sIsTabletValue = _isTablet(context); sIsTabletChecked = true; return sIsTabletValue; } /** * 判断是否是flyme系统 */ private static OnceReadValue isFlymeValue = new OnceReadValue() { @Override protected Boolean read(Void param) { checkReadInfo(); return !TextUtils.isEmpty(sFlymeVersionName) && sFlymeVersionName.contains(FLYME); } }; public static boolean isFlyme() { return isFlymeValue.get(null); } /** * 判断是否是MIUI系统 */ public static boolean isMIUI() { checkReadInfo(); return !TextUtils.isEmpty(sMiuiVersionName); } public static boolean isMIUIV5() { checkReadInfo(); return "v5".equals(sMiuiVersionName); } public static boolean isMIUIV6() { checkReadInfo(); return "v6".equals(sMiuiVersionName); } public static boolean isMIUIV7() { checkReadInfo(); return "v7".equals(sMiuiVersionName); } public static boolean isMIUIV8() { checkReadInfo(); return "v8".equals(sMiuiVersionName); } public static boolean isMIUIV9() { checkReadInfo(); return "v9".equals(sMiuiVersionName); } public static boolean isFlymeLowerThan(int majorVersion) { return isFlymeLowerThan(majorVersion, 0, 0); } public static boolean isFlymeLowerThan(int majorVersion, int minorVersion, int patchVersion) { checkReadInfo(); boolean isLower = false; if (sFlymeVersionName != null && !sFlymeVersionName.equals("")) { try { Pattern pattern = Pattern.compile("(\\d+\\.){2}\\d"); Matcher matcher = pattern.matcher(sFlymeVersionName); if (matcher.find()) { String versionString = matcher.group(); if (versionString.length() > 0) { String[] version = versionString.split("\\."); if (version.length >= 1) { if (Integer.parseInt(version[0]) < majorVersion) { isLower = true; } } if (version.length >= 2 && minorVersion > 0) { if (Integer.parseInt(version[1]) < majorVersion) { isLower = true; } } if (version.length >= 3 && patchVersion > 0) { if (Integer.parseInt(version[2]) < majorVersion) { isLower = true; } } } } } catch (Throwable ignore) { } } return isMeizu() && isLower; } private static OnceReadValue isMeizuValue = new OnceReadValue() { @Override protected Boolean read(Void param) { checkReadInfo(); return isPhone(MEIZUBOARD) || isFlyme(); } }; public static boolean isMeizu() { return isMeizuValue.get(null); } /** * 判断是否为小米 * https://dev.mi.com/doc/?p=254 */ private static OnceReadValue isXiaomiValue = new OnceReadValue() { @Override protected Boolean read(Void param) { return Build.MANUFACTURER.toLowerCase().equals("xiaomi"); } }; public static boolean isXiaomi() { return isXiaomiValue.get(null); } private static OnceReadValue isVivoValue = new OnceReadValue() { @Override protected Boolean read(Void param) { return BRAND.contains("vivo") || BRAND.contains("bbk"); } }; public static boolean isVivo() { return isVivoValue.get(null); } private static OnceReadValue isOppoValue = new OnceReadValue() { @Override protected Boolean read(Void param) { return BRAND.contains("oppo"); } }; public static boolean isOppo() { return isOppoValue.get(null); } private static OnceReadValue isHuaweiValue = new OnceReadValue() { @Override protected Boolean read(Void param) { return BRAND.contains("huawei") || BRAND.contains("honor"); } }; public static boolean isHuawei() { return isHuaweiValue.get(null); } private static OnceReadValue isEssentialPhoneValue = new OnceReadValue() { @Override protected Boolean read(Void param) { return BRAND.contains("essential"); } }; public static boolean isEssentialPhone() { return isEssentialPhoneValue.get(null); } private static OnceReadValue isMiuiFullDisplayValue = new OnceReadValue() { @Override protected Boolean read(Context param) { return isMIUI() && Settings.Global.getInt(param.getContentResolver(), "force_fsg_nav_bar", 0) != 0; } }; public static boolean isMiuiFullDisplay(Context context){ return isMiuiFullDisplayValue.get(context); } private static boolean isPhone(String[] boards) { checkReadInfo(); final String board = android.os.Build.BOARD; if (board == null) { return false; } for (String board1 : boards) { if (board.equals(board1)) { return true; } } return false; } public static long getTotalMemory(Context context) { if (sTotalMemory != -1) { return sTotalMemory; } ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (activityManager != null) { activityManager.getMemoryInfo(memoryInfo); sTotalMemory = memoryInfo.totalMem; } return sTotalMemory; } public static long getInnerStorageSize() { if (sInnerStorageSize != -1) { return sInnerStorageSize; } File dataDir = Environment.getDataDirectory(); if (dataDir == null) { return 0; } sInnerStorageSize = dataDir.getTotalSpace(); return sInnerStorageSize; } public static boolean hasExtraStorage() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } public static long getExtraStorageSize() { if (sExtraStorageSize != -1) { return sExtraStorageSize; } if (!hasExtraStorage()) { return 0; } File path = Environment.getExternalStorageDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSizeLong(); long availableBlocks = stat.getBlockCountLong(); sExtraStorageSize = blockSize * availableBlocks; return sExtraStorageSize; } public static long getTotalStorageSize() { return getInnerStorageSize() + getExtraStorageSize(); } // From Matrix public static int getCpuCoreCount() { if (sCpuCoreCount != -1) { return sCpuCoreCount; } int cores; try { cores = getCoresFromFile(CPU_FILE_PATH_1); if (cores == 0) { cores = getCoresFromFile(CPU_FILE_PATH_2); } if (cores == 0) { cores = getCoresFromCPUFiles(CPU_FILE_PATH_0); } } catch (Exception e) { cores = 0; } if (cores == 0) { cores = 1; } sCpuCoreCount = cores; return cores; } private static int getCoresFromCPUFiles(String path) { File[] list = new File(path).listFiles(CPU_FILTER); return null == list ? 0 : list.length; } private static int getCoresFromFile(String file) { InputStream is = null; try { is = new FileInputStream(file); BufferedReader buf = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); String fileContents = buf.readLine(); buf.close(); if (fileContents == null || !fileContents.matches("0-[\\d]+$")) { return 0; } String num = fileContents.substring(2); return Integer.parseInt(num) + 1; } catch (IOException e) { return 0; } finally { QMUILangHelper.close(is); } } /** * 判断悬浮窗权限(目前主要用户魅族与小米的检测)。 */ public static boolean isFloatWindowOpAllowed(Context context) { final int version = Build.VERSION.SDK_INT; return checkOp(context, 24); // 24 是AppOpsManager.OP_SYSTEM_ALERT_WINDOW 的值,该值无法直接访问 } public static double getBatteryCapacity(Context context) { if (sBatteryCapacity != -1) { return sBatteryCapacity; } double ret; try { Class cls = Class.forName(POWER_PROFILE_CLASS); Object instance = cls.getConstructor(Context.class).newInstance(context); Method method = cls.getMethod("getBatteryCapacity"); ret = (double) method.invoke(instance); } catch (Exception ignore) { ret = -1; } sBatteryCapacity = ret; return sBatteryCapacity; } private static boolean checkOp(Context context, int op) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { Method method = manager.getClass().getDeclaredMethod("checkOp", int.class, int.class, String.class); int property = (Integer) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); return AppOpsManager.MODE_ALLOWED == property; } catch (Exception e) { e.printStackTrace(); } return false; } @Nullable private static String getLowerCaseName(Properties p, Method get, String key) { String name = p.getProperty(key); if (name == null) { try { name = (String) get.invoke(null, key); } catch (Exception ignored) { } } if (name != null) name = name.toLowerCase(); return name; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDirection.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.util; /** * 定义了从左到右,从上到下,从右到左,从下到上四个方向的类 * Created by Kayo on 2017/2/7. */ public enum QMUIDirection { LEFT_TO_RIGHT, TOP_TO_BOTTOM, RIGHT_TO_LEFT, BOTTOM_TO_TOP } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDisplayHelper.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.util; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.net.ConnectivityManager; import android.os.Build; import android.os.Environment; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Display; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Locale; /** * @author cginechen * @date 2016-03-17 */ public class QMUIDisplayHelper { /** * 屏幕密度,系统源码注释不推荐使用 */ public static final float DENSITY = Resources.getSystem() .getDisplayMetrics().density; private static final String TAG = "QMUIDisplayHelper"; /** * 是否有摄像头 */ private static Boolean sHasCamera = null; // private static int[] sPortraitRealSizeCache = null; // private static int[] sLandscapeRealSizeCache = null; /** * 获取 DisplayMetrics * * @return */ public static DisplayMetrics getDisplayMetrics(Context context) { return context.getResources().getDisplayMetrics(); } /** * 把以 dp 为单位的值,转化为以 px 为单位的值 * * @param dpValue 以 dp 为单位的值 * @return px value */ public static int dpToPx(int dpValue) { return (int) (dpValue * DENSITY + 0.5f); } /** * 把以 px 为单位的值,转化为以 dp 为单位的值 * * @param pxValue 以 px 为单位的值 * @return dp值 */ public static int pxToDp(float pxValue) { return (int) (pxValue / DENSITY + 0.5f); } public static float getDensity(Context context) { return context.getResources().getDisplayMetrics().density; } public static float getFontDensity(Context context) { return context.getResources().getDisplayMetrics().scaledDensity; } /** * 获取屏幕宽度 * * @return */ public static int getScreenWidth(Context context) { return getDisplayMetrics(context).widthPixels; } /** * 获取屏幕高度 * * @return */ public static int getScreenHeight(Context context) { int screenHeight = getDisplayMetrics(context).heightPixels; if(QMUIDeviceHelper.isXiaomi() && xiaomiNavigationGestureEnabled(context)){ screenHeight += getResourceNavHeight(context); } return screenHeight; } /** * 获取屏幕的真实宽高 * * @param context * @return */ public static int[] getRealScreenSize(Context context) { // 切换屏幕导致宽高变化时不能用 cache,先去掉 cache return doGetRealScreenSize(context); // if (QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // // Essential Phone 8.0版本后,Display size 会根据挖孔屏的设置而得到不同的结果,不能信任 cache // return doGetRealScreenSize(context); // } // int orientation = context.getResources().getConfiguration().orientation; // int[] result; // if (orientation == Configuration.ORIENTATION_LANDSCAPE) { // result = sLandscapeRealSizeCache; // if (result == null) { // result = doGetRealScreenSize(context); // if(result[0] > result[1]){ // // the result may be wrong sometimes, do not cache !!!! // sLandscapeRealSizeCache = result; // } // } // return result; // } else { // result = sPortraitRealSizeCache; // if (result == null) { // result = doGetRealScreenSize(context); // if(result[0] < result[1]){ // // the result may be wrong sometimes, do not cache !!!! // sPortraitRealSizeCache = result; // } // } // return result; // } } private static int[] doGetRealScreenSize(Context context) { int[] size = new int[2]; int widthPixels, heightPixels; WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display d = w.getDefaultDisplay(); DisplayMetrics metrics = new DisplayMetrics(); d.getMetrics(metrics); // since SDK_INT = 1; widthPixels = metrics.widthPixels; heightPixels = metrics.heightPixels; try { // used when 17 > SDK_INT >= 14; includes window decorations (statusbar bar/menu bar) widthPixels = (Integer) Display.class.getMethod("getRawWidth").invoke(d); heightPixels = (Integer) Display.class.getMethod("getRawHeight").invoke(d); } catch (Exception ignored) { } if (Build.VERSION.SDK_INT >= 17) { try { // used when SDK_INT >= 17; includes window decorations (statusbar bar/menu bar) Point realSize = new Point(); d.getRealSize(realSize); Display.class.getMethod("getRealSize", Point.class).invoke(d, realSize); widthPixels = realSize.x; heightPixels = realSize.y; } catch (Exception ignored) { } } size[0] = widthPixels; size[1] = heightPixels; return size; } /** * 剔除挖孔屏等导致的不可用区域后的 width * * @param activity * @return */ public static int getUsefulScreenWidth(Activity activity) { return getUsefulScreenWidth(activity, QMUINotchHelper.hasNotch(activity)); } public static int getUsefulScreenWidth(View view) { return getUsefulScreenWidth(view.getContext(), QMUINotchHelper.hasNotch(view)); } public static int getUsefulScreenWidth(Context context, boolean hasNotch) { int result = getRealScreenSize(context)[0]; int orientation = context.getResources().getConfiguration().orientation; boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE; if (!hasNotch) { if (isLandscape && QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // https://arstechnica.com/gadgets/2017/09/essential-phone-review-impressive-for-a-new-company-but-not-competitive/ // 这里说挖孔屏是状态栏高度的两倍, 但横屏好像小了一点点 result -= 2 * QMUIStatusBarHelper.getStatusbarHeight(context); } return result; } if (isLandscape) { // 华为挖孔屏横屏时,会把整个 window 往后移动,因此,可用区域减小 if (QMUIDeviceHelper.isHuawei() && !QMUIDisplayHelper.huaweiIsNotchSetToShowInSetting(context)) { result -= QMUINotchHelper.getNotchSizeInHuawei(context)[1]; } // TODO vivo 设置-系统导航-导航手势样式-显示手势操作区域 打开的情况下,应该减去手势操作区域的高度,但无API // TODO vivo 设置-显示与亮度-第三方应用显示比例 选为安全区域显示时,整个 window 会移动,应该减去移动区域,但无API // TODO oppo 设置-显示与亮度-应用全屏显示-凹形区域显示控制 关闭是,整个 window 会移动,应该减去移动区域,但无API } return result; } /** * 剔除挖孔屏等导致的不可用区域后的 height * * @param activity * @return */ public static int getUsefulScreenHeight(Activity activity) { return getUsefulScreenHeight(activity, QMUINotchHelper.hasNotch(activity)); } public static int getUsefulScreenHeight(View view) { return getUsefulScreenHeight(view.getContext(), QMUINotchHelper.hasNotch(view)); } private static int getUsefulScreenHeight(Context context, boolean hasNotch) { int result = getRealScreenSize(context)[1]; int orientation = context.getResources().getConfiguration().orientation; boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT; if (!hasNotch) { if (isPortrait && QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // https://arstechnica.com/gadgets/2017/09/essential-phone-review-impressive-for-a-new-company-but-not-competitive/ // 这里说挖孔屏是状态栏高度的两倍 result -= 2 * QMUIStatusBarHelper.getStatusbarHeight(context); } return result; } // if (isPortrait) { // TODO vivo 设置-系统导航-导航手势样式-显示手势操作区域 打开的情况下,应该减去手势操作区域的高度,但无API // TODO vivo 设置-显示与亮度-第三方应用显示比例 选为安全区域显示时,整个 window 会移动,应该减去移动区域,但无API // TODO oppo 设置-显示与亮度-应用全屏显示-凹形区域显示控制 关闭是,整个 window 会移动,应该减去移动区域,但无API // } return result; } public static boolean isNavMenuExist(Context context) { //通过判断设备是否有返回键、菜单键(不是虚拟键,是手机屏幕外的按键)来确定是否有navigation bar boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey(); boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); if (!hasMenuKey && !hasBackKey) { // 做任何你需要做的,这个设备有一个导航栏 return true; } return false; } /** * 单位转换: dp -> px * * @param dp * @return */ public static int dp2px(Context context, int dp) { return (int) (getDensity(context) * dp + 0.5); } /** * 单位转换: sp -> px * * @param sp * @return */ public static int sp2px(Context context, int sp) { return (int) (getFontDensity(context) * sp + 0.5); } /** * 单位转换:px -> dp * * @param px * @return */ public static int px2dp(Context context, int px) { return (int) (px / getDensity(context) + 0.5); } /** * 单位转换:px -> sp * * @param px * @return */ public static int px2sp(Context context, int px) { return (int) (px / getFontDensity(context) + 0.5); } /** * 判断是否有状态栏 * * @param context * @return */ public static boolean hasStatusBar(Context context) { if (context instanceof Activity) { Activity activity = (Activity) context; WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); return (attrs.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN; } return true; } /** * 获取ActionBar高度 * * @param context * @return */ public static int getActionBarHeight(Context context) { int actionBarHeight = 0; TypedValue tv = new TypedValue(); if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics()); } return actionBarHeight; } /** * 获取状态栏高度 * * @param context * @return */ public static int getStatusBarHeight(Context context) { if(QMUIDeviceHelper.isXiaomi()){ int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return 0; } try { Class c = Class.forName("com.android.internal.R$dimen"); Object obj = c.newInstance(); Field field = c.getField("status_bar_height"); int x = Integer.parseInt(field.get(obj).toString()); if(x > 0){ return context.getResources().getDimensionPixelSize(x); } } catch (Exception e) { e.printStackTrace(); } return 0; } /** * 获取虚拟菜单的高度,若无则返回0 * * @param context * @return */ public static int getNavMenuHeight(Context context) { if (!isNavMenuExist(context)) { return 0; } int resourceNavHeight = getResourceNavHeight(context); if (resourceNavHeight >= 0) { return resourceNavHeight; } // 小米 MIX 有nav bar, 而 getRealScreenSize(context)[1] - getScreenHeight(context) = 0 return getRealScreenSize(context)[1] - getScreenHeight(context); } private static int getResourceNavHeight(Context context){ // 小米4没有nav bar, 而 navigation_bar_height 有值 int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return -1; } public static final boolean hasCamera(Context context) { if (sHasCamera == null) { PackageManager pckMgr = context.getPackageManager(); boolean flag = pckMgr .hasSystemFeature("android.hardware.camera.front"); boolean flag1 = pckMgr.hasSystemFeature("android.hardware.camera"); boolean flag2; flag2 = flag || flag1; sHasCamera = flag2; } return sHasCamera; } /** * 是否有硬件menu * * @param context * @return */ @SuppressWarnings("SimplifiableIfStatement") public static boolean hasHardwareMenuKey(Context context) { boolean flag; if (Build.VERSION.SDK_INT < 11) flag = true; else if (Build.VERSION.SDK_INT >= 14) { flag = ViewConfiguration.get(context).hasPermanentMenuKey(); } else flag = false; return flag; } /** * 是否有网络功能 * * @param context * @return */ @SuppressLint("MissingPermission") public static boolean hasInternet(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return cm.getActiveNetworkInfo() != null; } /** * 判断是否存在pckName包 * * @param pckName * @return */ public static boolean isPackageExist(Context context, String pckName) { try { PackageInfo pckInfo = context.getPackageManager() .getPackageInfo(pckName, 0); if (pckInfo != null) return true; } catch (PackageManager.NameNotFoundException ignored) { } return false; } /** * 判断 SD Card 是否 ready * * @return */ public static boolean isSdcardReady() { return Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState()); } /** * 获取当前国家的语言 * * @param context * @return */ public static String getCurCountryLan(Context context) { Configuration config = context.getResources().getConfiguration(); Locale sysLocale; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { sysLocale = config.getLocales().get(0); } else { //noinspection deprecation sysLocale = config.locale; } return sysLocale.getLanguage() + "-" + sysLocale.getCountry(); } /** * 判断是否为中文环境 * * @param context * @return */ public static boolean isZhCN(Context context) { Configuration config = context.getResources().getConfiguration(); Locale sysLocale; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { sysLocale = config.getLocales().get(0); } else { //noinspection deprecation sysLocale = config.locale; } String lang = sysLocale.getCountry(); return lang.equalsIgnoreCase("CN"); } /** * 设置全屏 * * @param activity */ public static void setFullScreen(Activity activity) { Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } /** * 取消全屏 * * @param activity */ public static void cancelFullScreen(Activity activity) { Window window = activity.getWindow(); window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); } /** * 判断是否全屏 * * @param activity * @return */ public static boolean isFullScreen(Activity activity) { WindowManager.LayoutParams params = activity.getWindow().getAttributes(); return (params.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN; } public static boolean isElevationSupported() { return android.os.Build.VERSION.SDK_INT >= 21; } public static boolean hasNavigationBar(Context context) { boolean hasNav = deviceHasNavigationBar(); if (!hasNav) { return false; } if (QMUIDeviceHelper.isVivo()) { return vivoNavigationGestureEnabled(context); } return true; } /** * 判断设备是否存在NavigationBar * * @return true 存在, false 不存在 */ private static boolean deviceHasNavigationBar() { boolean haveNav = false; try { //1.通过WindowManagerGlobal获取windowManagerService // 反射方法:IWindowManager windowManagerService = WindowManagerGlobal.getWindowManagerService(); Class windowManagerGlobalClass = Class.forName("android.view.WindowManagerGlobal"); Method getWmServiceMethod = windowManagerGlobalClass.getDeclaredMethod("getWindowManagerService"); getWmServiceMethod.setAccessible(true); //getWindowManagerService是静态方法,所以invoke null Object iWindowManager = getWmServiceMethod.invoke(null); //2.获取windowMangerService的hasNavigationBar方法返回值 // 反射方法:haveNav = windowManagerService.hasNavigationBar(); Class iWindowManagerClass = iWindowManager.getClass(); Method hasNavBarMethod = iWindowManagerClass.getDeclaredMethod("hasNavigationBar"); hasNavBarMethod.setAccessible(true); haveNav = (Boolean) hasNavBarMethod.invoke(iWindowManager); } catch (Exception e) { e.printStackTrace(); } return haveNav; } // ====================== Setting =========================== private static final String VIVO_NAVIGATION_GESTURE = "navigation_gesture_on"; private static final String HUAWAI_DISPLAY_NOTCH_STATUS = "display_notch_status"; private static final String XIAOMI_DISPLAY_NOTCH_STATUS = "force_black"; private static final String XIAOMI_FULLSCREEN_GESTURE = "force_fsg_nav_bar"; /** * 获取vivo手机设置中的"navigation_gesture_on"值,判断当前系统是使用导航键还是手势导航操作 * * @param context app Context * @return false 表示使用的是虚拟导航键(NavigationBar), true 表示使用的是手势, 默认是false */ public static boolean vivoNavigationGestureEnabled(Context context) { int val = Settings.Secure.getInt(context.getContentResolver(), VIVO_NAVIGATION_GESTURE, 0); return val != 0; } public static boolean xiaomiNavigationGestureEnabled(Context context) { int val = Settings.Global.getInt(context.getContentResolver(), XIAOMI_FULLSCREEN_GESTURE, 0); return val != 0; } public static boolean huaweiIsNotchSetToShowInSetting(Context context) { // 0: 默认 // 1: 隐藏显示区域 int result = Settings.Secure.getInt(context.getContentResolver(), HUAWAI_DISPLAY_NOTCH_STATUS, 0); return result == 0; } @TargetApi(17) public static boolean xiaomiIsNotchSetToShowInSetting(Context context) { return Settings.Global.getInt(context.getContentResolver(), XIAOMI_DISPLAY_NOTCH_STATUS, 0) == 0; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIDrawableHelper.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.util; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.LightingColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.DrawableCompat; import android.view.View; import android.widget.ImageView; import com.qmuiteam.qmui.QMUILog; /** * @author cginechen * @date 2016-03-17 */ public class QMUIDrawableHelper { private static final String TAG = QMUIDrawableHelper.class.getSimpleName(); //节省每次创建时产生的开销,但要注意多线程操作synchronized private static final Canvas sCanvas = new Canvas(); /** * 从一个view创建Bitmap。 * 注意点:绘制之前要清掉 View 的焦点,因为焦点可能会改变一个 View 的 UI 状态。 * 来源:https://github.com/tyrantgit/ExplosionField * * @param view 传入一个 View,会获取这个 View 的内容创建 Bitmap。 * @param scale 缩放比例,对创建的 Bitmap 进行缩放,数值支持从 0 到 1。 */ public static Bitmap createBitmapFromView(View view, float scale) { if (view instanceof ImageView) { Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } } view.clearFocus(); Bitmap bitmap = createBitmapSafely((int) (view.getWidth() * scale), (int) (view.getHeight() * scale), Bitmap.Config.ARGB_8888, 1); if (bitmap != null) { synchronized (sCanvas) { Canvas canvas = sCanvas; canvas.setBitmap(bitmap); canvas.save(); canvas.drawColor(Color.WHITE); // 防止 View 上面有些区域空白导致最终 Bitmap 上有些区域变黑 canvas.scale(scale, scale); view.draw(canvas); canvas.restore(); canvas.setBitmap(null); } } return bitmap; } public static Bitmap createBitmapFromView(View view) { return createBitmapFromView(view, 1f); } /** * 从一个view创建Bitmap。把view的区域截掉leftCrop/topCrop/rightCrop/bottomCrop */ public static Bitmap createBitmapFromView(View view, int leftCrop, int topCrop, int rightCrop, int bottomCrop) { Bitmap originBitmap = QMUIDrawableHelper.createBitmapFromView(view); if (originBitmap == null) { return null; } Bitmap cutBitmap = createBitmapSafely(view.getWidth() - rightCrop - leftCrop, view.getHeight() - topCrop - bottomCrop, Bitmap.Config.ARGB_8888, 1); if (cutBitmap == null) { return null; } Canvas canvas = new Canvas(cutBitmap); Rect src = new Rect(leftCrop, topCrop, view.getWidth() - rightCrop, view.getHeight() - bottomCrop); Rect dest = new Rect(0, 0, view.getWidth() - rightCrop - leftCrop, view.getHeight() - topCrop - bottomCrop); canvas.drawColor(Color.WHITE); // 防止 View 上面有些区域空白导致最终 Bitmap 上有些区域变黑 canvas.drawBitmap(originBitmap, src, dest, null); originBitmap.recycle(); return cutBitmap; } /** * 安全的创建bitmap。 * 如果新建 Bitmap 时产生了 OOM,可以主动进行一次 GC - System.gc(),然后再次尝试创建。 * * @param width Bitmap 宽度。 * @param height Bitmap 高度。 * @param config 传入一个 Bitmap.Config。 * @param retryCount 创建 Bitmap 时产生 OOM 后,主动重试的次数。 * @return 返回创建的 Bitmap。 */ public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) { try { return Bitmap.createBitmap(width, height, config); } catch (OutOfMemoryError e) { e.printStackTrace(); if (retryCount > 0) { System.gc(); return createBitmapSafely(width, height, config, retryCount - 1); } return null; } } /** * 创建一张指定大小的纯色图片,支持圆角 * * @param resources Resources对象,用于创建BitmapDrawable * @param width 图片的宽度 * @param height 图片的高度 * @param cornerRadius 图片的圆角,不需要则传0 * @param filledColor 图片的填充色 * @return 指定大小的纯色图片 */ public static BitmapDrawable createDrawableWithSize(Resources resources, int width, int height, int cornerRadius, @ColorInt int filledColor) { Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); if (filledColor == 0) { filledColor = Color.TRANSPARENT; } if (cornerRadius > 0) { Paint paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(filledColor); canvas.drawRoundRect(new RectF(0, 0, width, height), cornerRadius, cornerRadius, paint); } else { canvas.drawColor(filledColor); } return new BitmapDrawable(resources, output); } /** * 设置Drawable的颜色 * 这里不对Drawable进行mutate(),会影响到所有用到这个Drawable的地方,如果要避免,请先自行mutate() * * please use {@link DrawableCompat#setTint(Drawable, int)} replace this. */ @Deprecated public static ColorFilter setDrawableTintColor(Drawable drawable, @ColorInt int tintColor) { LightingColorFilter colorFilter = new LightingColorFilter(Color.argb(255, 0, 0, 0), tintColor); if(drawable != null){ drawable.setColorFilter(colorFilter); } return colorFilter; } /** * 由一个drawable生成bitmap */ public static Bitmap drawableToBitmap(Drawable drawable) { if (drawable == null) return null; else if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } int intrinsicWidth = drawable.getIntrinsicWidth(); int intrinsicHeight = drawable.getIntrinsicHeight(); if (!(intrinsicWidth > 0 && intrinsicHeight > 0)) return null; try { Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, config); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } catch (OutOfMemoryError e) { e.printStackTrace(); return null; } } /** * 创建一张渐变图片,支持韵脚。 * * @param startColor 渐变开始色 * @param endColor 渐变结束色 * @param radius 圆角大小 * @param centerX 渐变中心点 X 轴坐标 * @param centerY 渐变中心点 Y 轴坐标 * @return 返回所创建的渐变图片。 */ @TargetApi(16) public static GradientDrawable createCircleGradientDrawable(@ColorInt int startColor, @ColorInt int endColor, int radius, @FloatRange(from = 0f, to = 1f) float centerX, @FloatRange(from = 0f, to = 1f) float centerY) { GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setColors(new int[]{ startColor, endColor }); gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); gradientDrawable.setGradientRadius(radius); gradientDrawable.setGradientCenter(centerX, centerY); return gradientDrawable; } /** * 动态创建带上分隔线或下分隔线的Drawable。 * * @param separatorColor 分割线颜色。 * @param bgColor Drawable 的背景色。 * @param top true 则分割线为上分割线,false 则为下分割线。 * @return 返回所创建的 Drawable。 */ public static LayerDrawable createItemSeparatorBg(@ColorInt int separatorColor, @ColorInt int bgColor, int separatorHeight, boolean top) { ShapeDrawable separator = new ShapeDrawable(); separator.getPaint().setStyle(Paint.Style.FILL); separator.getPaint().setColor(separatorColor); ShapeDrawable bg = new ShapeDrawable(); bg.getPaint().setStyle(Paint.Style.FILL); bg.getPaint().setColor(bgColor); Drawable[] layers = {separator, bg}; LayerDrawable layerDrawable = new LayerDrawable(layers); layerDrawable.setLayerInset(1, 0, top ? separatorHeight : 0, 0, top ? 0 : separatorHeight); return layerDrawable; } /////////////// VectorDrawable ///////////////////// public static @Nullable Drawable getVectorDrawable(Context context, @DrawableRes int resVector) { try { return AppCompatResources.getDrawable(context, resVector); } catch (Exception e) { QMUILog.d(TAG, "Error in getVectorDrawable. resVector=" + resVector + ", resName=" + context.getResources().getResourceName(resVector) + e.getMessage()); return null; } } public static Bitmap vectorDrawableToBitmap(Context context, @DrawableRes int resVector) { Drawable drawable = getVectorDrawable(context, resVector); if (drawable != null) { Bitmap b = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); drawable.draw(c); return b; } return null; } /////////////// VectorDrawable ///////////////////// } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIKeyboardHelper.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.util; import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; import androidx.core.view.WindowInsetsCompat; import java.util.List; /** * @author cginechen * @date 2016-11-07 *

* https://github.com/yshrsmz/KeyboardVisibilityEvent/blob/master/keyboardvisibilityevent/src/main/java/net/yslibrary/android/keyboardvisibilityevent/KeyboardVisibilityEvent.java */ public class QMUIKeyboardHelper { /** * 显示软键盘的延迟时间 */ public static final int SHOW_KEYBOARD_DELAY_TIME = 200; private static final String TAG = "QMUIKeyboardHelper"; public final static int KEYBOARD_VISIBLE_THRESHOLD_DP = 100; public static void showKeyboard(final EditText editText, boolean delay) { showKeyboard(editText, delay ? SHOW_KEYBOARD_DELAY_TIME : 0); } /** * 针对给定的editText显示软键盘(editText会先获得焦点). 可以和{@link #hideKeyboard(View)} * 搭配使用,进行键盘的显示隐藏控制。 */ public static void showKeyboard(final EditText editText, int delay) { if (null == editText) return; if (!editText.requestFocus()) { Log.w(TAG, "showSoftInput() can not get focus"); return; } if (delay > 0) { editText.postDelayed(new Runnable() { @Override public void run() { InputMethodManager imm = (InputMethodManager) editText.getContext().getApplicationContext() .getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); } }, delay); } else { InputMethodManager imm = (InputMethodManager) editText.getContext().getApplicationContext() .getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); } } /** * 隐藏软键盘 可以和{@link #showKeyboard(EditText, boolean)}搭配使用,进行键盘的显示隐藏控制。 * * @param view 当前页面上任意一个可用的view */ public static boolean hideKeyboard(final View view) { if (null == view) return false; InputMethodManager inputManager = (InputMethodManager) view.getContext().getApplicationContext() .getSystemService(Context.INPUT_METHOD_SERVICE); // 即使当前焦点不在editText,也是可以隐藏的。 return inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } public static void listenKeyBoardWithOffsetSelf(final View view, final boolean minusNav){ ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) { @NonNull @Override public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List runningAnimations) { int height; Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); height = ime.bottom; if(minusNav){ Insets nav = insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()); height -= nav.bottom; } QMUIViewHelper.getOrCreateOffsetHelper(view).setTopAndBottomOffset(-height); return insets; } }); } public static void listenKeyBoardWithOffsetSelfHalf(final View view, final boolean minusNav){ ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) { @NonNull @Override public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List runningAnimations) { int height; Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); height = ime.bottom; if(minusNav){ Insets nav = insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()); height -= nav.bottom; } QMUIViewHelper.getOrCreateOffsetHelper(view).setTopAndBottomOffset(-height / 2); return insets; } }); } /** * Set keyboard visibility change event listener. * * @param activity Activity * @param listener KeyboardVisibilityEventListener */ @SuppressWarnings("deprecation") public static void setVisibilityEventListener(final Activity activity, final KeyboardVisibilityEventListener listener) { if (activity == null) { throw new NullPointerException("Parameter:activity must not be null"); } if (listener == null) { throw new NullPointerException("Parameter:listener must not be null"); } final View activityRoot = QMUIViewHelper.getActivityRoot(activity); final ViewTreeObserver.OnGlobalLayoutListener layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { private final Rect r = new Rect(); private final int visibleThreshold = Math.round( QMUIDisplayHelper.dp2px(activity, KEYBOARD_VISIBLE_THRESHOLD_DP)); private boolean wasOpened = false; @Override public void onGlobalLayout() { activityRoot.getWindowVisibleDisplayFrame(r); int heightDiff = activityRoot.getRootView().getHeight() - r.height(); boolean isOpen = heightDiff > visibleThreshold; if (isOpen == wasOpened) { // keyboard state has not changed return; } wasOpened = isOpen; boolean removeListener = listener.onVisibilityChanged(isOpen, heightDiff); if (removeListener) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { activityRoot.getViewTreeObserver() .removeOnGlobalLayoutListener(this); } else { activityRoot.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } } } }; activityRoot.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); activity.getApplication() .registerActivityLifecycleCallbacks(new QMUIActivityLifecycleCallbacks(activity) { @Override protected void onTargetActivityDestroyed() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { activityRoot.getViewTreeObserver() .removeOnGlobalLayoutListener(layoutListener); } else { activityRoot.getViewTreeObserver() .removeGlobalOnLayoutListener(layoutListener); } } }); } /** * Determine if keyboard is visible * * @param activity Activity * @return Whether keyboard is visible or not */ public static boolean isKeyboardVisible(Activity activity) { Rect r = new Rect(); View activityRoot = QMUIViewHelper.getActivityRoot(activity); int visibleThreshold = Math.round(QMUIDisplayHelper.dp2px(activity, KEYBOARD_VISIBLE_THRESHOLD_DP)); activityRoot.getWindowVisibleDisplayFrame(r); int heightDiff = activityRoot.getRootView().getHeight() - r.height(); return heightDiff > visibleThreshold; } public interface KeyboardVisibilityEventListener { /** * @return to remove global listener or not */ boolean onVisibilityChanged(boolean isOpen, int heightDiff); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUILangHelper.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.util; import androidx.annotation.Nullable; import java.io.Closeable; import java.io.IOException; import java.util.Locale; import java.util.Objects; /** * @author cginechen * @date 2016-03-17 */ public class QMUILangHelper { /** * 获取数值的位数,例如9返回1,99返回2,999返回3 * * @param number 要计算位数的数值,必须>0 * @return 数值的位数,若传的参数小于等于0,则返回0 */ public static int getNumberDigits(int number) { if (number <= 0) return 0; return (int) (Math.log10(number) + 1); } public static int getNumberDigits(long number) { if (number <= 0) return 0; return (int) (Math.log10(number) + 1); } public static String formatNumberToLimitedDigits(int number, int maxDigits) { if (getNumberDigits(number) > maxDigits) { StringBuilder result = new StringBuilder(); for (int digit = 1; digit <= maxDigits; digit++) { result.append("9"); } result.append("+"); return result.toString(); } else { return String.valueOf(number); } } /** * 规范化价格字符串显示的工具类 * * @param price 价格 * @return 保留两位小数的价格字符串 */ public static String regularizePrice(float price) { return String.format(Locale.CHINESE, "%.2f", price); } /** * 规范化价格字符串显示的工具类 * * @param price 价格 * @return 保留两位小数的价格字符串 */ public static String regularizePrice(double price) { return String.format(Locale.CHINESE, "%.2f", price); } public static boolean isNullOrEmpty(@Nullable CharSequence string) { return string == null || string.length() == 0; } public static void close(Closeable c) { if (c != null) { try { c.close(); } catch (IOException e) { e.printStackTrace(); } } } @Deprecated public static boolean objectEquals(Object a, Object b) { return Objects.equals(a, b); } public static int constrain(int amount, int low, int high) { return amount < low ? low : (amount > high ? high : amount); } public static float constrain(float amount, float low, float high) { return amount < low ? low : (amount > high ? high : amount); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUINotchHelper.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.util; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.util.Log; import android.view.Display; import android.view.DisplayCutout; import android.view.Surface; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import java.lang.reflect.Method; public class QMUINotchHelper { private static final String TAG = "QMUINotchHelper"; private static final int NOTCH_IN_SCREEN_VOIO = 0x00000020; private static final String MIUI_NOTCH = "ro.miui.notch"; private static Boolean sHasNotch = null; private static Rect sRotation0SafeInset = null; private static Rect sRotation90SafeInset = null; private static Rect sRotation180SafeInset = null; private static Rect sRotation270SafeInset = null; private static int[] sNotchSizeInHawei = null; private static Boolean sHuaweiIsNotchSetToShow = null; public static boolean hasNotchInVivo(Context context) { boolean ret = false; try { ClassLoader cl = context.getClassLoader(); Class ftFeature = cl.loadClass("android.util.FtFeature"); Method[] methods = ftFeature.getDeclaredMethods(); if (methods != null) { for (int i = 0; i < methods.length; i++) { Method method = methods[i]; if (method.getName().equalsIgnoreCase("isFeatureSupport")) { ret = (boolean) method.invoke(ftFeature, NOTCH_IN_SCREEN_VOIO); break; } } } } catch (ClassNotFoundException e) { Log.i(TAG, "hasNotchInVivo ClassNotFoundException"); } catch (Exception e) { Log.e(TAG, "hasNotchInVivo Exception"); } return ret; } public static boolean hasNotchInHuawei(Context context) { boolean hasNotch = false; try { ClassLoader cl = context.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen"); hasNotch = (boolean) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { Log.i(TAG, "hasNotchInHuawei ClassNotFoundException"); } catch (NoSuchMethodException e) { Log.e(TAG, "hasNotchInHuawei NoSuchMethodException"); } catch (Exception e) { Log.e(TAG, "hasNotchInHuawei Exception"); } return hasNotch; } public static boolean hasNotchInOppo(Context context) { return context.getPackageManager() .hasSystemFeature("com.oppo.feature.screen.heteromorphism"); } @SuppressLint("PrivateApi") public static boolean hasNotchInXiaomi(Context context) { try { Class spClass = Class.forName("android.os.SystemProperties"); Method getMethod = spClass.getDeclaredMethod("getInt", String.class, int.class); getMethod.setAccessible(true); int hasNotch = (int) getMethod.invoke(null, MIUI_NOTCH, 0); return hasNotch == 1; } catch (Exception e) { e.printStackTrace(); } return false; } public static boolean hasNotch(View view){ if (sHasNotch == null) { if(isNotchOfficialSupport()){ if(!attachHasOfficialNotch(view)){ return false; } }else { sHasNotch = has3rdNotch(view.getContext()); } } return sHasNotch; } public static boolean hasNotch(Activity activity) { if (sHasNotch == null) { if(isNotchOfficialSupport()){ Window window = activity.getWindow(); if(window == null){ return false; } View decorView = window.getDecorView(); if(decorView == null){ return false; } if(!attachHasOfficialNotch(decorView)){ return false; } }else { sHasNotch = has3rdNotch(activity); } } return sHasNotch; } /** * * @param view * @return false indicates the failure to get the result */ @TargetApi(28) private static boolean attachHasOfficialNotch(View view){ WindowInsets windowInsets = view.getRootWindowInsets(); if(windowInsets != null){ DisplayCutout displayCutout = windowInsets.getDisplayCutout(); sHasNotch = displayCutout != null; return true; }else{ // view not attached, do nothing return false; } } public static boolean has3rdNotch(Context context){ if (QMUIDeviceHelper.isHuawei()) { return hasNotchInHuawei(context); } else if (QMUIDeviceHelper.isVivo()) { return hasNotchInVivo(context); } else if (QMUIDeviceHelper.isOppo()) { return hasNotchInOppo(context); } else if (QMUIDeviceHelper.isXiaomi()) { return hasNotchInXiaomi(context); } return false; } public static int getSafeInsetTop(Activity activity) { if (!hasNotch(activity)) { return 0; } return getSafeInsetRect(activity).top; } public static int getSafeInsetBottom(Activity activity) { if (!hasNotch(activity)) { return 0; } return getSafeInsetRect(activity).bottom; } public static int getSafeInsetLeft(Activity activity) { if (!hasNotch(activity)) { return 0; } return getSafeInsetRect(activity).left; } public static int getSafeInsetRight(Activity activity) { if (!hasNotch(activity)) { return 0; } return getSafeInsetRect(activity).right; } public static int getSafeInsetTop(View view) { if (!hasNotch(view)) { return 0; } return getSafeInsetRect(view).top; } public static int getSafeInsetBottom(View view) { if (!hasNotch(view)) { return 0; } return getSafeInsetRect(view).bottom; } public static int getSafeInsetLeft(View view) { if (!hasNotch(view)) { return 0; } return getSafeInsetRect(view).left; } public static int getSafeInsetRight(View view) { if (!hasNotch(view)) { return 0; } return getSafeInsetRect(view).right; } private static void clearAllRectInfo() { sRotation0SafeInset = null; sRotation90SafeInset = null; sRotation180SafeInset = null; sRotation270SafeInset = null; } private static void clearPortraitRectInfo() { sRotation0SafeInset = null; sRotation180SafeInset = null; } private static void clearLandscapeRectInfo() { sRotation90SafeInset = null; sRotation270SafeInset = null; } private static Rect getSafeInsetRect(Activity activity) { if(isNotchOfficialSupport()){ Rect rect = new Rect(); View decorView = activity.getWindow().getDecorView(); getOfficialSafeInsetRect(decorView, rect); return rect; } return get3rdSafeInsetRect(activity); } private static Rect getSafeInsetRect(View view) { if(isNotchOfficialSupport()){ Rect rect = new Rect(); getOfficialSafeInsetRect(view, rect); return rect; } return get3rdSafeInsetRect(view.getContext()); } @TargetApi(28) private static void getOfficialSafeInsetRect(View view, Rect out) { if(view == null){ return; } WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); if(rootWindowInsets == null){ return; } Insets cutoutInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); out.set(cutoutInsets.left, cutoutInsets.top, cutoutInsets.right, cutoutInsets.bottom); } private static Rect get3rdSafeInsetRect(Context context){ // 全面屏设置项更改 if (QMUIDeviceHelper.isHuawei()) { boolean isHuaweiNotchSetToShow = QMUIDisplayHelper.huaweiIsNotchSetToShowInSetting(context); if (sHuaweiIsNotchSetToShow != null && sHuaweiIsNotchSetToShow != isHuaweiNotchSetToShow) { clearLandscapeRectInfo(); } sHuaweiIsNotchSetToShow = isHuaweiNotchSetToShow; } int screenRotation = getScreenRotation(context); if (screenRotation == Surface.ROTATION_90) { if (sRotation90SafeInset == null) { sRotation90SafeInset = getRectInfoRotation90(context); } return sRotation90SafeInset; } else if (screenRotation == Surface.ROTATION_180) { if (sRotation180SafeInset == null) { sRotation180SafeInset = getRectInfoRotation180(context); } return sRotation180SafeInset; } else if (screenRotation == Surface.ROTATION_270) { if (sRotation270SafeInset == null) { sRotation270SafeInset = getRectInfoRotation270(context); } return sRotation270SafeInset; } else { if (sRotation0SafeInset == null) { sRotation0SafeInset = getRectInfoRotation0(context); } return sRotation0SafeInset; } } private static Rect getRectInfoRotation0(Context context) { Rect rect = new Rect(); if (QMUIDeviceHelper.isVivo()) { // TODO vivo 显示与亮度-第三方应用显示比例 rect.top = getNotchHeightInVivo(context); rect.bottom = 0; } else if (QMUIDeviceHelper.isOppo()) { // TODO OPPO 设置-显示-应用全屏显示-凹形区域显示控制 rect.top = QMUIStatusBarHelper.getStatusbarHeight(context); rect.bottom = 0; } else if (QMUIDeviceHelper.isHuawei()) { int[] notchSize = getNotchSizeInHuawei(context); rect.top = notchSize[1]; rect.bottom = 0; } else if (QMUIDeviceHelper.isXiaomi()) { rect.top = getNotchHeightInXiaomi(context); rect.bottom = 0; } return rect; } private static Rect getRectInfoRotation90(Context context) { Rect rect = new Rect(); if (QMUIDeviceHelper.isVivo()) { rect.left = getNotchHeightInVivo(context); rect.right = 0; } else if (QMUIDeviceHelper.isOppo()) { rect.left = QMUIStatusBarHelper.getStatusbarHeight(context); rect.right = 0; } else if (QMUIDeviceHelper.isHuawei()) { if (sHuaweiIsNotchSetToShow) { rect.left = getNotchSizeInHuawei(context)[1]; } else { rect.left = 0; } rect.right = 0; } else if (QMUIDeviceHelper.isXiaomi()) { rect.left = getNotchHeightInXiaomi(context); rect.right = 0; } return rect; } private static Rect getRectInfoRotation180(Context context) { Rect rect = new Rect(); if (QMUIDeviceHelper.isVivo()) { rect.top = 0; rect.bottom = getNotchHeightInVivo(context); } else if (QMUIDeviceHelper.isOppo()) { rect.top = 0; rect.bottom = QMUIStatusBarHelper.getStatusbarHeight(context); } else if (QMUIDeviceHelper.isHuawei()) { int[] notchSize = getNotchSizeInHuawei(context); rect.top = 0; rect.bottom = notchSize[1]; } else if (QMUIDeviceHelper.isXiaomi()) { rect.top = 0; rect.bottom = getNotchHeightInXiaomi(context); } return rect; } private static Rect getRectInfoRotation270(Context context) { Rect rect = new Rect(); if (QMUIDeviceHelper.isVivo()) { rect.right = getNotchHeightInVivo(context); rect.left = 0; } else if (QMUIDeviceHelper.isOppo()) { rect.right = QMUIStatusBarHelper.getStatusbarHeight(context); rect.left = 0; } else if (QMUIDeviceHelper.isHuawei()) { if (sHuaweiIsNotchSetToShow) { rect.right = getNotchSizeInHuawei(context)[1]; } else { rect.right = 0; } rect.left = 0; } else if (QMUIDeviceHelper.isXiaomi()) { rect.right = getNotchHeightInXiaomi(context); rect.left = 0; } return rect; } public static int[] getNotchSizeInHuawei(Context context) { if (sNotchSizeInHawei == null) { sNotchSizeInHawei = new int[]{0, 0}; try { ClassLoader cl = context.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("getNotchSize"); sNotchSizeInHawei = (int[]) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { Log.e(TAG, "getNotchSizeInHuawei ClassNotFoundException"); } catch (NoSuchMethodException e) { Log.e(TAG, "getNotchSizeInHuawei NoSuchMethodException"); } catch (Exception e) { Log.e(TAG, "getNotchSizeInHuawei Exception"); } } return sNotchSizeInHawei; } public static int getNotchWidthInXiaomi(Context context) { int resourceId = context.getResources().getIdentifier("notch_width", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return -1; } public static int getNotchHeightInXiaomi(Context context) { int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android"); if (resourceId > 0) { return context.getResources().getDimensionPixelSize(resourceId); } return QMUIDisplayHelper.getStatusBarHeight(context); } public static int getNotchWidthInVivo(Context context){ return QMUIDisplayHelper.dp2px(context, 100); } public static int getNotchHeightInVivo(Context context){ return QMUIDisplayHelper.dp2px(context, 27); } /** * this method is private, because we do not need to handle tablet * * @param context * @return */ private static int getScreenRotation(Context context) { WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (w == null) { return Surface.ROTATION_0; } Display display = w.getDefaultDisplay(); if (display == null) { return Surface.ROTATION_0; } return display.getRotation(); } public static boolean isNotchOfficialSupport(){ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; } /** * fitSystemWindows 对小米、vivo挖孔屏横屏挖孔区域无效 * @param view * @return */ public static boolean needFixLandscapeNotchAreaFitSystemWindow(View view){ return (QMUIDeviceHelper.isXiaomi() || QMUIDeviceHelper.isVivo()) && QMUINotchHelper.hasNotch(view); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIPackageHelper.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.util; import android.content.Context; import android.content.pm.PackageManager; /** * Created by Kayo on 2016/11/18. * */ public class QMUIPackageHelper { private static String appVersionName; private static String majorMinorVersion; private static int majorVersion = -1; private static int minorVersion = -1; private static int fixVersion = -1; /** * manifest 中的 versionName 字段 */ public static String getAppVersion(Context context) { if (appVersionName == null) { PackageManager manager = context.getPackageManager(); try { android.content.pm.PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); appVersionName = info.versionName; } catch (Exception e) { e.printStackTrace(); } } if (appVersionName == null) { return ""; } else { return appVersionName; } } /** * 获取 App 的主版本与次版本号。比如说 3.1.2 中的 3.1 */ public static String getMajorMinorVersion(Context context) { if (majorMinorVersion == null || majorMinorVersion.equals("")) { majorMinorVersion = getMajorVersion(context) + "." + getMinorVersion(context); } return majorMinorVersion; } /** * 读取 App 的主版本号,例如 3.1.2,主版本号是 3 */ private static int getMajorVersion(Context context) { if (majorVersion == -1) { String appVersion = getAppVersion(context); String[] parts = appVersion.split("\\."); if (parts.length != 0) { majorVersion = Integer.parseInt(parts[0]); } } return majorVersion; } /** * 读取 App 的次版本号,例如 3.1.2,次版本号是 1 */ private static int getMinorVersion(Context context) { if (minorVersion == -1) { String appVersion = getAppVersion(context); String[] parts = appVersion.split("\\."); if (parts.length >= 2) { minorVersion = Integer.parseInt(parts[1]); } } return minorVersion; } /** * 读取 App 的修正版本号,例如 3.1.2,修正版本号是 2 */ public static int getFixVersion(Context context) { if (fixVersion == -1) { String appVersion = getAppVersion(context); String[] parts = appVersion.split("\\."); if (parts.length >= 3) { fixVersion = Integer.parseInt(parts[2]); } } return fixVersion; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIReflectHelper.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.util; import android.util.Log; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; // Modify from https://github.com/didi/booster/blob/master/booster-android-instrument/src/main/java/com/didiglobal/booster/instrument/Reflection.java public class QMUIReflectHelper { private static final String TAG = "QMUIReflectHelper"; private QMUIReflectHelper() { } @SuppressWarnings("unchecked") public static T getStaticFieldValue(final Class cls, final String name) { if (null != cls && null != name) { try { final Field field = getField(cls, name); if (null != field) { field.setAccessible(true); return (T) field.get(cls); } } catch (final Throwable t) { Log.w(TAG, "get static field " + name + " of " + cls + " error", t); } } return null; } public static boolean setStaticFieldValue(final Class cls, final String name, final Object value) { if (null != cls && null != name) { try { final Field field = getField(cls, name); if (null != field) { field.setAccessible(true); field.set(cls, value); return true; } } catch (final Throwable t) { Log.w(TAG, "set static field " + name + " of " + cls + " error", t); } } return false; } @SuppressWarnings("unchecked") public static T getFieldValue(final Object obj, final String name) { if (null != obj && null != name) { try { final Field field = getField(obj.getClass(), name); if (null != field) { field.setAccessible(true); return (T) field.get(obj); } } catch (final Throwable t) { Log.w(TAG, "get field " + name + " of " + obj + " error", t); } } return null; } @SuppressWarnings("unchecked") public static T getFieldValue(final Object obj, final Class type) { if (null != obj && null != type) { try { final Field field = getField(obj.getClass(), type); if (null != field) { field.setAccessible(true); return (T) field.get(obj); } } catch (final Throwable t) { Log.w(TAG, "get field with type " + type + " of " + obj + " error", t); } } return null; } public static boolean setFieldValue(final Object obj, final String name, final Object value) { if (null != obj && null != name) { try { final Field field = getField(obj.getClass(), name); if (null != field) { field.setAccessible(true); field.set(obj, value); return true; } } catch (final Throwable t) { Log.w(TAG, "set field " + name + " of " + obj + " error", t); } } return false; } public static T newInstance(final String className, final Object... args) { try { return newInstance(Class.forName(className), args); } catch (final ClassNotFoundException e) { Log.w(TAG, "new instance of " + className + " error", e); return null; } } @SuppressWarnings("unchecked") public static T newInstance(final Class clazz, Object... args) { final Constructor[] ctors = clazz.getDeclaredConstructors(); loop: for (final Constructor ctor : ctors) { final Class[] types = ctor.getParameterTypes(); if (types.length == args.length) { for (int i = 0; i < types.length; i++) { if (null != args[i] && !types[i].isAssignableFrom(args[i].getClass())) { continue loop; } } try { ctor.setAccessible(true); return (T) ctor.newInstance(args); } catch (final Throwable t) { Log.w(TAG, "Invoke constructor " + ctor + " error", t); return null; } } } return null; } @SuppressWarnings("unchecked") public static T invokeStaticMethod(final Class klass, final String name) { return invokeStaticMethod(klass, name, new Class[0], new Object[0]); } @SuppressWarnings("unchecked") public static T invokeStaticMethod(final Class klass, final String name, final Class[] types, final Object[] args) { if (null != klass && null != name && null != types && null != args && types.length == args.length) { try { final Method method = getMethod(klass, name, types); if (null != method) { method.setAccessible(true); return (T) method.invoke(klass, args); } } catch (final Throwable e) { Log.w(TAG, "Invoke " + name + "(" + Arrays.toString(types) + ") of " + klass + " error", e); } } return null; } @SuppressWarnings("unchecked") public static T invokeMethod(final Object obj, final String name) { return invokeMethod(obj, name, new Class[0], new Object[0]); } @SuppressWarnings("unchecked") public static T invokeMethod(final Object obj, final String name, final Class[] types, final Object[] args) { if (null != obj && null != name && null != types && null != args && types.length == args.length) { try { final Method method = getMethod(obj.getClass(), name, types); if (null != method) { method.setAccessible(true); return (T) method.invoke(obj, args); } } catch (final Throwable e) { Log.w(TAG, "Invoke " + name + "(" + Arrays.toString(types) + ") of " + obj + " error", e); } } return null; } public static Field getField(final Class cls, final String name) { try { return cls.getDeclaredField(name); } catch (final NoSuchFieldException e) { final Class parent = cls.getSuperclass(); if (null == parent) { return null; } return getField(parent, name); } } public static Field getField(final Class cls, final Class type) { final Field[] fields = cls.getDeclaredFields(); if (fields.length <= 0) { final Class parent = cls.getSuperclass(); if (null == parent) { return null; } return getField(parent, type); } for (final Field field : fields) { if (field.getType() == type) { return field; } } return null; } private static Method getMethod(final Class cls, final String name, final Class[] types) { try { return cls.getDeclaredMethod(name, types); } catch (final NoSuchMethodException e) { final Class parent = cls.getSuperclass(); if (null == parent) { return null; } return getMethod(parent, name, types); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIResHelper.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.util; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.TypedValue; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.R; /** * @author cginechen * @date 2016-09-22 */ public class QMUIResHelper { private static TypedValue sTmpValue; public static float getAttrFloatValue(Context context, int attr) { return getAttrFloatValue(context.getTheme(), attr); } public static float getAttrFloatValue(Resources.Theme theme, int attr) { if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!theme.resolveAttribute(attr, sTmpValue, true)) { return 0; } return sTmpValue.getFloat(); } public static int getAttrColor(Context context, int attrRes) { return getAttrColor(context.getTheme(), attrRes); } public static int getAttrColor(Resources.Theme theme, int attr) { if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!theme.resolveAttribute(attr, sTmpValue, true)) { return 0; } if (sTmpValue.type == TypedValue.TYPE_ATTRIBUTE) { return getAttrColor(theme, sTmpValue.data); } return sTmpValue.data; } @Nullable public static ColorStateList getAttrColorStateList(Context context, int attrRes) { return getAttrColorStateList(context, context.getTheme(), attrRes); } @Nullable public static ColorStateList getAttrColorStateList(Context context, Resources.Theme theme, int attr) { if (attr == 0) { return null; } if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!theme.resolveAttribute(attr, sTmpValue, true)) { return null; } if (sTmpValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && sTmpValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { return ColorStateList.valueOf(sTmpValue.data); } if (sTmpValue.type == TypedValue.TYPE_ATTRIBUTE) { return getAttrColorStateList(context, theme, sTmpValue.data); } if (sTmpValue.resourceId == 0) { return null; } return ContextCompat.getColorStateList(context, sTmpValue.resourceId); } @Nullable public static Drawable getAttrDrawable(Context context, int attr) { return getAttrDrawable(context, context.getTheme(), attr); } @Nullable public static Drawable getAttrDrawable(Context context, Resources.Theme theme, int attr) { if (attr == 0) { return null; } if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!theme.resolveAttribute(attr, sTmpValue, true)) { return null; } if (sTmpValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && sTmpValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { return new ColorDrawable(sTmpValue.data); } if (sTmpValue.type == TypedValue.TYPE_ATTRIBUTE) { return getAttrDrawable(context, theme, sTmpValue.data); } if (sTmpValue.resourceId != 0) { return QMUIDrawableHelper.getVectorDrawable(context, sTmpValue.resourceId); } return null; } @Nullable public static Drawable getAttrDrawable(Context context, TypedArray typedArray, int index) { TypedValue value = typedArray.peekValue(index); if (value != null) { if (value.type != TypedValue.TYPE_ATTRIBUTE && value.resourceId != 0) { return QMUIDrawableHelper.getVectorDrawable(context, value.resourceId); } } return null; } public static int getAttrDimen(Context context, int attrRes) { if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!context.getTheme().resolveAttribute(attrRes, sTmpValue, true)) { return 0; } return TypedValue.complexToDimensionPixelSize(sTmpValue.data, QMUIDisplayHelper.getDisplayMetrics(context)); } @Nullable public static String getAttrString(Context context, int attrRes) { if (sTmpValue == null) { sTmpValue = new TypedValue(); } if (!context.getTheme().resolveAttribute(attrRes, sTmpValue, true)) { return null; } CharSequence str = sTmpValue.string; return str == null ? null : str.toString(); } public static int getAttrInt(Context context, int attrRes) { if (sTmpValue == null) { sTmpValue = new TypedValue(); } context.getTheme().resolveAttribute(attrRes, sTmpValue, true); return sTmpValue.data; } public static void assignTextViewWithAttr(TextView textView, int attrRes) { TypedArray a = textView.getContext().obtainStyledAttributes(null, R.styleable.QMUITextCommonStyleDef, attrRes, 0); int count = a.getIndexCount(); int paddingLeft = textView.getPaddingLeft(), paddingRight = textView.getPaddingRight(), paddingTop = textView.getPaddingTop(), paddingBottom = textView.getPaddingBottom(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUITextCommonStyleDef_android_gravity) { textView.setGravity(a.getInt(attr, -1)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_textColor) { textView.setTextColor(a.getColorStateList(attr)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_textSize) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, a.getDimensionPixelSize(attr, 0)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_paddingLeft) { paddingLeft = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_paddingRight) { paddingRight = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_paddingTop) { paddingTop = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_paddingBottom) { paddingBottom = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_singleLine) { textView.setSingleLine(a.getBoolean(attr, false)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_ellipsize) { int ellipsize = a.getInt(attr, 3); switch (ellipsize) { case 1: textView.setEllipsize(TextUtils.TruncateAt.START); break; case 2: textView.setEllipsize(TextUtils.TruncateAt.MIDDLE); break; case 3: textView.setEllipsize(TextUtils.TruncateAt.END); break; case 4: textView.setEllipsize(TextUtils.TruncateAt.MARQUEE); break; } } else if (attr == R.styleable.QMUITextCommonStyleDef_android_maxLines) { textView.setMaxLines(a.getInt(attr, -1)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_background) { QMUIViewHelper.setBackgroundKeepingPadding(textView, a.getDrawable(attr)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_lineSpacingExtra) { textView.setLineSpacing(a.getDimensionPixelSize(attr, 0), 1f); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_drawablePadding) { textView.setCompoundDrawablePadding(a.getDimensionPixelSize(attr, 0)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_textColorHint) { textView.setHintTextColor(a.getColor(attr, 0)); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_textStyle) { int styleIndex = a.getInt(attr, -1); textView.setTypeface(null, styleIndex); } } textView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); a.recycle(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUISpanHelper.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.util; import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.view.View; import com.qmuiteam.qmui.span.QMUIAlignMiddleImageSpan; import com.qmuiteam.qmui.span.QMUIMarginImageSpan; import androidx.annotation.Nullable; /** * @author cginechen * @date 2016-10-12 */ public class QMUISpanHelper { /** * 在text左边或者右边添加icon, * 默认TextView添加leftDrawable或rightDrawable不能适应TextView match_parent的情况 * * @param left true 则在文字左边添加 icon,false 则在文字右边添加 icon * @param text 文字内容 * @param icon 需要被添加的 icon * @return 返回带有 icon 的文字 */ public static CharSequence generateSideIconText(boolean left, int iconPadding, CharSequence text, Drawable icon) { return generateSideIconText( left, iconPadding, text, icon, 0); } public static CharSequence generateSideIconText(boolean left, int iconPadding, CharSequence text, Drawable icon, int iconOffsetY){ return generateSideIconText( left, iconPadding, text, icon, iconOffsetY, 0, null); } public static CharSequence generateSideIconText(boolean left, int iconPadding, CharSequence text, Drawable icon, int iconTintAttr, @Nullable View skinFollowView){ return generateSideIconText( left, iconPadding, text, icon, 0, iconTintAttr, skinFollowView); } public static CharSequence generateSideIconText(boolean left, int iconPadding, CharSequence text, Drawable icon, int iconOffsetY, int iconTintAttr, @Nullable View skinFollowView) { return generateHorIconText(text, left ? iconPadding : 0, left ? icon : null, left ? iconTintAttr : 0, left ? 0 : iconPadding, left ? null : icon, left ? 0 : iconTintAttr, iconOffsetY, skinFollowView); } public static CharSequence generateHorIconText(CharSequence text, int leftPadding, Drawable iconLeft, int rightPadding, Drawable iconRight) { return generateHorIconText(text, leftPadding, iconLeft, rightPadding, iconRight,0); } public static CharSequence generateHorIconText(CharSequence text, int leftPadding, Drawable iconLeft, int rightPadding, Drawable iconRight, int iconOffsetY) { return generateHorIconText(text, leftPadding, iconLeft, 0, rightPadding, iconRight, 0, iconOffsetY, null); } public static CharSequence generateHorIconText(CharSequence text, int leftPadding, Drawable iconLeft, int iconLeftTintAttr, int rightPadding, Drawable iconRight, int iconRightTintAttr, @Nullable View skinFollowView) { return generateHorIconText(text, leftPadding, iconLeft, iconLeftTintAttr, rightPadding, iconRight, iconRightTintAttr,0, skinFollowView); } public static CharSequence generateHorIconText(CharSequence text, int leftPadding, Drawable iconLeft, int iconLeftTintAttr, int rightPadding, Drawable iconRight, int iconRightTintAttr, int iconOffsetY, @Nullable View skinFollowView) { if (iconLeft == null && iconRight == null) { return text; } String iconTag = "[icon]"; SpannableStringBuilder builder = new SpannableStringBuilder(); int start, end; if (iconLeft != null) { iconLeft.setBounds(0, 0, iconLeft.getIntrinsicWidth(), iconLeft.getIntrinsicHeight()); start = 0; builder.append(iconTag); end = builder.length(); QMUIMarginImageSpan imageSpan = new QMUIMarginImageSpan(iconLeft, QMUIAlignMiddleImageSpan.ALIGN_MIDDLE, 0, leftPadding, iconOffsetY); imageSpan.setSkinSupportWithTintColor(skinFollowView, iconLeftTintAttr); imageSpan.setAvoidSuperChangeFontMetrics(true); builder.setSpan(imageSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } builder.append(text); if (iconRight != null) { iconRight.setBounds(0, 0, iconRight.getIntrinsicWidth(), iconRight.getIntrinsicHeight()); start = builder.length(); builder.append(iconTag); end = builder.length(); QMUIMarginImageSpan imageSpan = new QMUIMarginImageSpan(iconRight, QMUIAlignMiddleImageSpan.ALIGN_MIDDLE, rightPadding, 0, iconOffsetY); imageSpan.setSkinSupportWithTintColor(skinFollowView, iconRightTintAttr); imageSpan.setAvoidSuperChangeFontMetrics(true); builder.setSpan(imageSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } return builder; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIStatusBarHelper.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.util; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.os.Build; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.ColorInt; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsControllerCompat; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * @author cginechen * @date 2016-03-27 */ public class QMUIStatusBarHelper { private enum StatusBarType { Default, Miui, Flyme, Android6 } private final static int STATUS_BAR_DEFAULT_HEIGHT_DP = 25; // 大部分状态栏都是25dp // 在某些机子上存在不同的density值,所以增加两个虚拟值 public static float sVirtualDensity = -1; public static float sVirtualDensityDpi = -1; private static int sStatusBarHeight = -1; private static StatusBarType mStatusBarType = StatusBarType.Default; private static Integer sTransparentValue; public static void translucent(Activity activity) { translucent(activity.getWindow()); } public static void translucent(Window window) { translucent(window, 0x40000000); } private static boolean supportTranslucent() { // Essential Phone 在 Android 8 之前沉浸式做得不全,系统不从状态栏顶部开始布局却会下发 WindowInsets return !(QMUIDeviceHelper.isEssentialPhone() && Build.VERSION.SDK_INT < 26); } /** * 沉浸式状态栏。 * 支持 4.4 以上版本的 MIUI 和 Flyme,以及 5.0 以上版本的其他 Android。 * * @param activity 需要被设置沉浸式状态栏的 Activity。 */ public static void translucent(Activity activity, @ColorInt int colorOn5x) { Window window = activity.getWindow(); translucent(window, colorOn5x); } @TargetApi(19) public static void translucent(Window window, @ColorInt int colorOn5x) { if (!supportTranslucent()) { // 版本小于4.4,绝对不考虑沉浸式 return; } if (QMUINotchHelper.isNotchOfficialSupport()) { handleDisplayCutoutMode(window); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 小米 Android 6.0 ,开发版 7.7.13 及以后版本设置黑色字体又需要 clear FLAG_TRANSLUCENT_STATUS, 因此还原为官方模式 if (QMUIDeviceHelper.isFlymeLowerThan(8) || (QMUIDeviceHelper.isMIUI() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M)) { window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); return; } } int systemUiVisibility = window.getDecorView().getSystemUiVisibility(); systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; window.getDecorView().setSystemUiVisibility(systemUiVisibility); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // android 6以后可以改状态栏字体颜色,因此可以自行设置为透明 window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(Color.TRANSPARENT); } else { // android 5不能修改状态栏字体颜色,因此直接用FLAG_TRANSLUCENT_STATUS,nexus表现为半透明 // 魅族和小米的表现如何? // update: 部分手机运用FLAG_TRANSLUCENT_STATUS时背景不是半透明而是没有背景了。。。。。 // window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); // 采取setStatusBarColor的方式,部分机型不支持,那就纯黑了,保证状态栏图标可见 window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setStatusBarColor(colorOn5x); } } /** * 如果原本存在某一个flag, 就将它迁移到 out * @param window * @param out * @param type * @return */ public static int retainSystemUiFlag(Window window, int out, int type) { int now = window.getDecorView().getSystemUiVisibility(); if ((now & type) == type) { out |= type; } return out; } @TargetApi(28) private static void handleDisplayCutoutMode(final Window window) { View decorView = window.getDecorView(); if (decorView != null) { if (ViewCompat.isAttachedToWindow(decorView)) { realHandleDisplayCutoutMode(window, decorView); } else { decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { v.removeOnAttachStateChangeListener(this); realHandleDisplayCutoutMode(window, v); } @Override public void onViewDetachedFromWindow(View v) { } }); } } } @TargetApi(28) private static void realHandleDisplayCutoutMode(Window window, View decorView) { if (decorView.getRootWindowInsets() != null && decorView.getRootWindowInsets().getDisplayCutout() != null) { WindowManager.LayoutParams params = window.getAttributes(); params.layoutInDisplayCutoutMode = WindowManager.LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; window.setAttributes(params); } } /** * 设置状态栏黑色字体图标, * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android * * @param activity 需要被处理的 Activity */ public static boolean setStatusBarLightMode(Activity activity) { if (activity == null) return false; if (mStatusBarType != StatusBarType.Default) { return setStatusBarLightMode(activity, mStatusBarType); } if (isMIUICustomStatusBarLightModeImpl() && MIUISetStatusBarLightMode(activity.getWindow(), true)) { mStatusBarType = StatusBarType.Miui; return true; } else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) { mStatusBarType = StatusBarType.Flyme; return true; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Android6SetStatusBarLightMode(activity.getWindow(), true); mStatusBarType = StatusBarType.Android6; return true; } return false; } /** * 已知系统类型时,设置状态栏黑色字体图标。 * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android * * @param activity 需要被处理的 Activity * @param type StatusBar 类型,对应不同的系统 */ private static boolean setStatusBarLightMode(Activity activity, StatusBarType type) { if (type == StatusBarType.Miui) { return MIUISetStatusBarLightMode(activity.getWindow(), true); } else if (type == StatusBarType.Flyme) { return FlymeSetStatusBarLightMode(activity.getWindow(), true); } else if (type == StatusBarType.Android6) { return Android6SetStatusBarLightMode(activity.getWindow(), true); } return false; } /** * 设置状态栏白色字体图标 * 支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android */ public static boolean setStatusBarDarkMode(Activity activity) { if (activity == null) return false; if (mStatusBarType == StatusBarType.Default) { // 默认状态,不需要处理 return true; } if (mStatusBarType == StatusBarType.Miui) { return MIUISetStatusBarLightMode(activity.getWindow(), false); } else if (mStatusBarType == StatusBarType.Flyme) { return FlymeSetStatusBarLightMode(activity.getWindow(), false); } else if (mStatusBarType == StatusBarType.Android6) { return Android6SetStatusBarLightMode(activity.getWindow(), false); } return true; } /** * 设置状态栏字体图标为深色,Android 6 * * @param window 需要设置的窗口 * @param light 是否把状态栏字体及图标颜色设置为深色 * @return boolean 成功执行返回true */ @TargetApi(23) private static boolean Android6SetStatusBarLightMode(Window window, boolean light) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(window, window.getDecorView()); if (insetsController != null) { insetsController.setAppearanceLightStatusBars(light); } } else { // 经过测试,小米 Android 11 用 WindowInsetsControllerCompat 不起作用, 我还能说什么呢。。。 View decorView = window.getDecorView(); int systemUi = decorView.getSystemUiVisibility(); if (light) { systemUi |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } else { systemUi &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } decorView.setSystemUiVisibility(systemUi); } if (QMUIDeviceHelper.isMIUIV9()) { // MIUI 9 低于 6.0 版本依旧只能回退到以前的方案 // https://github.com/Tencent/QMUI_Android/issues/160 MIUISetStatusBarLightMode(window, light); } return true; } /** * 设置状态栏字体图标为深色,需要 MIUIV6 以上 * * @param window 需要设置的窗口 * @param light 是否把状态栏字体及图标颜色设置为深色 * @return boolean 成功执行返回 true */ @SuppressWarnings("unchecked") public static boolean MIUISetStatusBarLightMode(Window window, boolean light) { boolean result = false; if (window != null) { Class clazz = window.getClass(); try { int darkModeFlag; Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); darkModeFlag = field.getInt(layoutParams); Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); if (light) { extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体 } else { extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体 } result = true; } catch (Exception ignored) { } } return result; } /** * 更改状态栏图标、文字颜色的方案是否是MIUI自家的, MIUI9 && Android 6 之后用回Android原生实现 * 见小米开发文档说明:https://dev.mi.com/console/doc/detail?pId=1159 */ private static boolean isMIUICustomStatusBarLightModeImpl() { if (QMUIDeviceHelper.isMIUIV9() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } return QMUIDeviceHelper.isMIUIV5() || QMUIDeviceHelper.isMIUIV6() || QMUIDeviceHelper.isMIUIV7() || QMUIDeviceHelper.isMIUIV8(); } /** * 设置状态栏图标为深色和魅族特定的文字风格 * 可以用来判断是否为 Flyme 用户 * * @param window 需要设置的窗口 * @param light 是否把状态栏字体及图标颜色设置为深色 * @return boolean 成功执行返回true */ public static boolean FlymeSetStatusBarLightMode(Window window, boolean light) { boolean result = false; if (window != null) { Android6SetStatusBarLightMode(window, light); // flyme 在 6.2.0.0A 支持了 Android 官方的实现方案,旧的方案失效 // 高版本调用这个出现不可预期的 Bug,官方文档也没有给出完整的高低版本兼容方案 if (QMUIDeviceHelper.isFlymeLowerThan(7)) { try { WindowManager.LayoutParams lp = window.getAttributes(); Field darkFlag = WindowManager.LayoutParams.class .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); Field meizuFlags = WindowManager.LayoutParams.class .getDeclaredField("meizuFlags"); darkFlag.setAccessible(true); meizuFlags.setAccessible(true); int bit = darkFlag.getInt(null); int value = meizuFlags.getInt(lp); if (light) { value |= bit; } else { value &= ~bit; } meizuFlags.setInt(lp, value); window.setAttributes(lp); result = true; } catch (Exception ignored) { } } else if (QMUIDeviceHelper.isFlyme()) { result = true; } } return result; } /** * 获取是否全屏 * * @return 是否全屏 */ public static boolean isFullScreen(Activity activity) { boolean ret = false; try { WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); ret = (attrs.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0; } catch (Exception e) { e.printStackTrace(); } return ret; } /** * API19之前透明状态栏:获取设置透明状态栏的system ui visibility的值,这是部分有提供接口的rom使用的 * http://stackoverflow.com/questions/21865621/transparent-status-bar-before-4-4-kitkat */ public static Integer getStatusBarAPITransparentValue(Context context) { if (sTransparentValue != null) { return sTransparentValue; } String[] systemSharedLibraryNames = context.getPackageManager() .getSystemSharedLibraryNames(); String fieldName = null; for (String lib : systemSharedLibraryNames) { if ("touchwiz".equals(lib)) { fieldName = "SYSTEM_UI_FLAG_TRANSPARENT_BACKGROUND"; } else if (lib.startsWith("com.sonyericsson.navigationbar")) { fieldName = "SYSTEM_UI_FLAG_TRANSPARENT"; } } if (fieldName != null) { try { Field field = View.class.getField(fieldName); if (field != null) { Class type = field.getType(); if (type == int.class) { sTransparentValue = field.getInt(null); } } } catch (Exception ignored) { } } return sTransparentValue; } /** * 获取状态栏的高度。 */ public static int getStatusbarHeight(Context context) { if (sStatusBarHeight == -1) { initStatusBarHeight(context); } return sStatusBarHeight; } private static void initStatusBarHeight(Context context) { Class clazz; Object obj = null; Field field = null; try { clazz = Class.forName("com.android.internal.R$dimen"); obj = clazz.newInstance(); if (QMUIDeviceHelper.isMeizu()) { try { field = clazz.getField("status_bar_height_large"); } catch (Throwable t) { t.printStackTrace(); } } if (field == null) { field = clazz.getField("status_bar_height"); } } catch (Throwable t) { t.printStackTrace(); } if (field != null && obj != null) { try { int id = Integer.parseInt(field.get(obj).toString()); sStatusBarHeight = context.getResources().getDimensionPixelSize(id); } catch (Throwable t) { t.printStackTrace(); } } if (sStatusBarHeight <= 0) { if (sVirtualDensity == -1) { sStatusBarHeight = QMUIDisplayHelper.dp2px(context, STATUS_BAR_DEFAULT_HEIGHT_DP); } else { sStatusBarHeight = (int) (STATUS_BAR_DEFAULT_HEIGHT_DP * sVirtualDensity + 0.5f); } } } public static void setVirtualDensity(float density) { sVirtualDensity = density; } public static void setVirtualDensityDpi(float densityDpi) { sVirtualDensityDpi = densityDpi; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIToastHelper.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.util; import android.os.Build; import android.os.Handler; import android.os.Message; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; // Modify from https://github.com/didi/booster/blob/master/booster-android-instrument-toast/src/main/java/com/didiglobal/booster/instrument/ShadowToast.java public class QMUIToastHelper { private static final String TAG = "QMUIToastHelper"; public static void show(Toast toast){ if(Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1){ fixToastForAndroidN(toast).show(); }else{ toast.show(); } } private static Toast fixToastForAndroidN(Toast toast){ Object tn = QMUIReflectHelper.getFieldValue(toast, "mTN"); if(tn == null){ Log.w(TAG, "The value of field mTN of " + toast + " is null"); return toast; } Object handler = QMUIReflectHelper.getFieldValue(tn, "mHandler"); if(handler instanceof Handler){ if(QMUIReflectHelper.setFieldValue( handler, "mCallback", new FixCallback((Handler) handler))){ return toast; } } final Object show = QMUIReflectHelper.getFieldValue(tn, "mShow"); if (show instanceof Runnable) { if (QMUIReflectHelper.setFieldValue(tn, "mShow", new FixRunnable((Runnable) show))) { return toast; } } Log.w(TAG, "Neither field mHandler nor mShow of " + tn + " is accessible"); return toast; } public static class FixCallback implements Handler.Callback { private final Handler mHandler; public FixCallback(final Handler handler) { mHandler = handler; } @Override public boolean handleMessage(@NonNull Message msg) { try { mHandler.handleMessage(msg); } catch (Throwable e) { // ignore } return true; } } public static class FixRunnable implements Runnable { private final Runnable mRunnable; public FixRunnable(final Runnable runnable) { mRunnable = runnable; } @Override public void run() { try { mRunnable.run(); } catch (final RuntimeException e) { // ignore } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.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.util; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.LightingColorFilter; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.TranslateAnimation; import android.widget.ImageView; import android.widget.ListView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * @author cginechen * @date 2016-03-17 */ public class QMUIViewHelper { // copy from View.generateViewId for API <= 16 private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); private static final int[] APPCOMPAT_CHECK_ATTRS = { androidx.appcompat.R.attr.colorPrimary }; public static void checkAppCompatTheme(Context context) { TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS); final boolean failed = !a.hasValue(0); a.recycle(); if (failed) { throw new IllegalArgumentException("You need to use a Theme.AppCompat theme " + "(or descendant) with the design library."); } } /** * 获取activity的根view */ public static View getActivityRoot(Activity activity) { return ((ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT)).getChildAt(0); } /** * 触发window的insets的广播,使得view的fitSystemWindows得以生效 */ @SuppressWarnings("deprecation") public static void requestApplyInsets(Window window) { if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 21) { window.getDecorView().requestFitSystemWindows(); } else if (Build.VERSION.SDK_INT >= 21) { window.getDecorView().requestApplyInsets(); } } /** * 扩展点击区域的范围 * * @param view 需要扩展的元素,此元素必需要有父级元素 * @param expendSize 需要扩展的尺寸(以sp为单位的) */ public static void expendTouchArea(final View view, final int expendSize) { if (view != null) { final View parentView = (View) view.getParent(); parentView.post(new Runnable() { @Override public void run() { Rect rect = new Rect(); view.getHitRect(rect); //如果太早执行本函数,会获取rect失败,因为此时UI界面尚未开始绘制,无法获得正确的坐标 rect.left -= expendSize; rect.top -= expendSize; rect.right += expendSize; rect.bottom += expendSize; parentView.setTouchDelegate(new TouchDelegate(rect, view)); } }); } } @SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public static void setBackground(View view, Drawable drawable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(drawable); } else { view.setBackgroundDrawable(drawable); } } public static void setBackgroundKeepingPadding(View view, Drawable drawable) { int[] padding = new int[]{view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()}; view.setBackground(drawable); view.setPadding(padding[0], padding[1], padding[2], padding[3]); } @SuppressWarnings("deprecation") public static void setBackgroundKeepingPadding(View view, int backgroundResId) { setBackgroundKeepingPadding(view, ContextCompat.getDrawable(view.getContext(), backgroundResId)); } public static void setBackgroundColorKeepPadding(View view, @ColorInt int color) { int[] padding = new int[]{view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()}; view.setBackgroundColor(color); view.setPadding(padding[0], padding[1], padding[2], padding[3]); } /** * 对 View 的做背景闪动的动画 */ public static void playBackgroundBlinkAnimation(final View v, @ColorInt int bgColor) { if (v == null) { return; } int[] alphaArray = new int[]{0, 255, 0}; playViewBackgroundAnimation(v, bgColor, alphaArray, 300); } /** * 对 View 做背景色变化的动作 * * @param v 做背景色变化的View * @param bgColor 背景色 * @param alphaArray 背景色变化的alpha数组,如 int[]{255,0} 表示从纯色变化到透明 * @param stepDuration 每一步变化的时长 * @param endAction 动画结束后的回调 */ public static Animator playViewBackgroundAnimation(final View v, @ColorInt int bgColor, int[] alphaArray, int stepDuration, final Runnable endAction) { int animationCount = alphaArray.length - 1; Drawable bgDrawable = new ColorDrawable(bgColor); final Drawable oldBgDrawable = v.getBackground(); setBackgroundKeepingPadding(v, bgDrawable); List animatorList = new ArrayList<>(); for (int i = 0; i < animationCount; i++) { ObjectAnimator animator = ObjectAnimator.ofInt(v.getBackground(), "alpha", alphaArray[i], alphaArray[i + 1]); animatorList.add(animator); } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(stepDuration); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { setBackgroundKeepingPadding(v, oldBgDrawable); if (endAction != null) { endAction.run(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorSet.playSequentially(animatorList); animatorSet.start(); return animatorSet; } public static void playViewBackgroundAnimation(final View v, @ColorInt int bgColor, int[] alphaArray, int stepDuration) { playViewBackgroundAnimation(v, bgColor, alphaArray, stepDuration, null); } /** * 对 View 做背景色变化的动作 * * @param v 做背景色变化的View * @param startColor 动画开始时 View 的背景色 * @param endColor 动画结束时 View 的背景色 * @param duration 动画总时长 * @param repeatCount 动画重复次数 * @param setAnimTagId 将动画设置tag给view,若为0则不设置 * @param endAction 动画结束后的回调 */ public static void playViewBackgroundAnimation(final View v, @ColorInt int startColor, @ColorInt int endColor, long duration, int repeatCount, int setAnimTagId, final Runnable endAction) { final Drawable oldBgDrawable = v.getBackground(); // 存储旧的背景 QMUIViewHelper.setBackgroundColorKeepPadding(v, startColor); final ValueAnimator anim = new ValueAnimator(); anim.setIntValues(startColor, endColor); anim.setDuration(duration / (repeatCount + 1)); anim.setRepeatCount(repeatCount); anim.setRepeatMode(ValueAnimator.REVERSE); anim.setEvaluator(new ArgbEvaluator()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { QMUIViewHelper.setBackgroundColorKeepPadding(v, (Integer) animation.getAnimatedValue()); } }); if (setAnimTagId != 0) { v.setTag(setAnimTagId, anim); } anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { setBackgroundKeepingPadding(v, oldBgDrawable); if (endAction != null) { endAction.run(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); anim.start(); } public static void playViewBackgroundAnimation(final View v, int startColor, int endColor, long duration) { playViewBackgroundAnimation(v, startColor, endColor, duration, 0, 0, null); } public static int generateViewId() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return View.generateViewId(); } else { for (; ; ) { final int result = sNextGeneratedId.get(); // aapt-generated IDs have the high byte nonzero; clamp to the range under that. int newValue = result + 1; if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. if (sNextGeneratedId.compareAndSet(result, newValue)) { return result; } } } } /** *

对 View 做透明度变化的进场动画。

*

相关方法 {@link #fadeOut(View, int, Animation.AnimationListener, boolean)}

* * @param view 做动画的 View * @param duration 动画时长(毫秒) * @param listener 动画回调 * @param isNeedAnimation 是否需要动画 */ public static AlphaAnimation fadeIn(View view, int duration, Animation.AnimationListener listener, boolean isNeedAnimation) { if (view == null) { return null; } if (isNeedAnimation) { view.setVisibility(View.VISIBLE); AlphaAnimation alpha = new AlphaAnimation(0, 1); alpha.setInterpolator(new DecelerateInterpolator()); alpha.setDuration(duration); alpha.setFillAfter(true); if (listener != null) { alpha.setAnimationListener(listener); } view.startAnimation(alpha); return alpha; } else { view.setAlpha(1); view.setVisibility(View.VISIBLE); return null; } } /** *

对 View 做透明度变化的退场动画

*

相关方法 {@link #fadeIn(View, int, Animation.AnimationListener, boolean)}

* * @param view 做动画的 View * @param duration 动画时长(毫秒) * @param listener 动画回调 * @param isNeedAnimation 是否需要动画 */ public static AlphaAnimation fadeOut(final View view, int duration, final Animation.AnimationListener listener, boolean isNeedAnimation) { if (view == null) { return null; } if (isNeedAnimation) { AlphaAnimation alpha = new AlphaAnimation(1, 0); alpha.setInterpolator(new DecelerateInterpolator()); alpha.setDuration(duration); alpha.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (listener != null) { listener.onAnimationStart(animation); } } @Override public void onAnimationEnd(Animation animation) { view.setVisibility(View.GONE); if (listener != null) { listener.onAnimationEnd(animation); } } @Override public void onAnimationRepeat(Animation animation) { if (listener != null) { listener.onAnimationRepeat(animation); } } }); view.startAnimation(alpha); return alpha; } else { view.setVisibility(View.GONE); return null; } } public static void clearValueAnimator(Animator animator) { if (animator != null) { animator.removeAllListeners(); if (animator instanceof ValueAnimator) { ((ValueAnimator) animator).removeAllUpdateListeners(); } if (Build.VERSION.SDK_INT >= 19) { animator.pause(); } animator.cancel(); } } public static Rect calcViewScreenLocation(View view) { int[] location = new int[2]; // 获取控件在屏幕中的位置,返回的数组分别为控件左顶点的 x、y 的值 view.getLocationOnScreen(location); return new Rect(location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight()); } /** *

对 View 做上下位移的进场动画

*

相关方法 {@link #slideOut(View, int, Animation.AnimationListener, boolean, QMUIDirection)}

* * @param view 做动画的 View * @param duration 动画时长(毫秒) * @param listener 动画回调 * @param isNeedAnimation 是否需要动画 * @param direction 进场动画的方向 * @return 动画对应的 Animator 对象, 注意无动画时返回 null */ public static @Nullable TranslateAnimation slideIn(final View view, int duration, final Animation.AnimationListener listener, boolean isNeedAnimation, QMUIDirection direction) { if (view == null) { return null; } if (isNeedAnimation) { TranslateAnimation translate = null; switch (direction) { case LEFT_TO_RIGHT: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f ); break; case TOP_TO_BOTTOM: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f ); break; case RIGHT_TO_LEFT: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f ); break; case BOTTOM_TO_TOP: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0f ); break; } translate.setInterpolator(new DecelerateInterpolator()); translate.setDuration(duration); translate.setFillAfter(true); if (listener != null) { translate.setAnimationListener(listener); } view.setVisibility(View.VISIBLE); view.startAnimation(translate); return translate; } else { view.clearAnimation(); view.setVisibility(View.VISIBLE); return null; } } /** *

对 View 做上下位移的退场动画

*

相关方法 {@link #slideIn(View, int, Animation.AnimationListener, boolean, QMUIDirection)}

* * @param view 做动画的 View * @param duration 动画时长(毫秒) * @param listener 动画回调 * @param isNeedAnimation 是否需要动画 * @param direction 进场动画的方向 * @return 动画对应的 Animator 对象, 注意无动画时返回 null */ public static @Nullable TranslateAnimation slideOut(final View view, int duration, final Animation.AnimationListener listener, boolean isNeedAnimation, QMUIDirection direction) { if (view == null) { return null; } if (isNeedAnimation) { TranslateAnimation translate = null; switch (direction) { case LEFT_TO_RIGHT: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f ); break; case TOP_TO_BOTTOM: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 1f ); break; case RIGHT_TO_LEFT: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f ); break; case BOTTOM_TO_TOP: translate = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f ); break; } translate.setInterpolator(new DecelerateInterpolator()); translate.setDuration(duration); translate.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (listener != null) { listener.onAnimationStart(animation); } } @Override public void onAnimationEnd(Animation animation) { view.setVisibility(View.GONE); if (listener != null) { listener.onAnimationEnd(animation); } } @Override public void onAnimationRepeat(Animation animation) { if (listener != null) { listener.onAnimationRepeat(animation); } } }); view.startAnimation(translate); return translate; } else { view.clearAnimation(); view.setVisibility(View.GONE); return null; } } /** * 对 View 设置 paddingLeft * * @param view 需要被设置的 View * @param value 设置的值 */ public static void setPaddingLeft(View view, int value) { if (value != view.getPaddingLeft()) { view.setPadding(value, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } } /** * 对 View 设置 paddingTop * * @param view 需要被设置的 View * @param value 设置的值 */ public static void setPaddingTop(View view, int value) { if (value != view.getPaddingTop()) { view.setPadding(view.getPaddingLeft(), value, view.getPaddingRight(), view.getPaddingBottom()); } } /** * 对 View 设置 paddingRight * * @param view 需要被设置的 View * @param value 设置的值 */ public static void setPaddingRight(View view, int value) { if (value != view.getPaddingRight()) { view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), value, view.getPaddingBottom()); } } /** * 对 View 设置 paddingBottom * * @param view 需要被设置的 View * @param value 设置的值 */ public static void setPaddingBottom(View view, int value) { if (value != view.getPaddingBottom()) { view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), value); } } public static void updateChildrenOffsetHelperOnLayout(@NonNull ViewGroup viewGroup){ View view; QMUIViewOffsetHelper offsetHelper; for(int i = 0; i < viewGroup.getChildCount(); i++){ view = viewGroup.getChildAt(i); offsetHelper = getOffsetHelper(view); if(offsetHelper != null){ offsetHelper.onViewLayout(); } } } @Nullable public static QMUIViewOffsetHelper getOffsetHelper(@NonNull View view){ Object tag = view.getTag(R.id.qmui_view_offset_helper); if(tag instanceof QMUIViewOffsetHelper){ return (QMUIViewOffsetHelper) tag; } return null; } @NonNull public static QMUIViewOffsetHelper getOrCreateOffsetHelper(@NonNull View view){ Object tag = view.getTag(R.id.qmui_view_offset_helper); if(tag instanceof QMUIViewOffsetHelper){ return (QMUIViewOffsetHelper) tag; }else{ QMUIViewOffsetHelper ret = new QMUIViewOffsetHelper(view); view.setTag(R.id.qmui_view_offset_helper, ret); return ret; } } /** * requestDisallowInterceptTouchEvent 的安全方法。存在它的原因是 QMUIPullRefreshLayout 会拦截这个事件 * * @param view * @param value */ public static void safeRequestDisallowInterceptTouchEvent(@NonNull View view, boolean value) { ViewParent viewParent = view.getParent(); if (viewParent != null) { ViewParent layout = viewParent; while (layout != null) { if (layout instanceof QMUIPullRefreshLayout) { ((QMUIPullRefreshLayout) layout).openSafeDisallowInterceptTouchEvent(); } layout = layout.getParent(); } viewParent.requestDisallowInterceptTouchEvent(value); } } public static void safeSetImageViewSelected(ImageView imageView, boolean selected) { // imageView setSelected 实现有问题。 // resizeFromDrawable 中判断 drawable size 是否改变而调用 requestLayout,看似合理,但不会被调用 // 因为 super.setSelected(selected) 会调用 refreshDrawableState // 而从 android 6 以后, ImageView 会重载refreshDrawableState,并在里面处理了 drawable size 改变的问题, // 从而导致 resizeFromDrawable 的判断失效 Drawable drawable = imageView.getDrawable(); if (drawable == null) { return; } int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); imageView.setSelected(selected); if (drawable.getIntrinsicWidth() != drawableWidth || drawable.getIntrinsicHeight() != drawableHeight) { imageView.requestLayout(); } } /** * please use ImageViewCompat.setImageTintList() replace this. */ @Deprecated public static ColorFilter setImageViewTintColor(ImageView imageView, @ColorInt int tintColor) { LightingColorFilter colorFilter = new LightingColorFilter(Color.argb(255, 0, 0, 0), tintColor); imageView.setColorFilter(colorFilter); return colorFilter; } /** * 判断 ListView 是否已经滚动到底部。 * * @param listView 需要被判断的 ListView。 * @return ListView 已经滚动到底部则返回 true,否则返回 false。 */ public static boolean isListViewAlreadyAtBottom(ListView listView) { if (listView.getAdapter() == null || listView.getHeight() == 0) { return false; } if (listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1) { View lastItemView = listView.getChildAt(listView.getChildCount() - 1); if (lastItemView != null && lastItemView.getBottom() == listView.getHeight()) { return true; } } return false; } /** * Retrieve the transformed bounding rect of an arbitrary descendant view. * This does not need to be a direct child. * * @param descendant descendant view to reference * @param out rect to set to the bounds of the descendant view */ public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) { out.set(0, 0, descendant.getWidth(), descendant.getHeight()); ViewGroupHelper.offsetDescendantRect(parent, descendant, out); } public static boolean getDescendantVisibleRect(ViewGroup target, View descendant, Rect out){ out.set(0, 0, descendant.getWidth(), descendant.getHeight()); ViewParent parent = descendant.getParent(); View next = descendant; while (parent instanceof ViewGroup && parent != target){ final ViewGroup vp = (ViewGroup) parent; ViewGroupHelper.offsetDescendantRect(vp, next, out); if(out.left >= vp.getWidth() || out.right <= 0 || out.top >= vp.getHeight() || out.bottom <= 0){ return false; } if(out.left < 0){ out.left = 0; } if(out.right > vp.getWidth()){ out.right = vp.getWidth(); } if(out.top < 0){ out.top = 0; } if(out.bottom > vp.getHeight()){ out.bottom = vp.getHeight(); } next = vp; parent = parent.getParent(); } return out.left < target.getWidth() && out.right > 0 && out.top < target.getHeight() && out.bottom > 0; } private static class ViewGroupHelper { private static final ThreadLocal sMatrix = new ThreadLocal<>(); private static final ThreadLocal sRectF = new ThreadLocal<>(); public static void offsetDescendantRect(ViewGroup group, View child, Rect rect) { Matrix m = sMatrix.get(); if (m == null) { m = new Matrix(); sMatrix.set(m); } else { m.reset(); } m.preTranslate(-group.getScrollX(), -group.getScrollY()); offsetDescendantMatrix(group, child, m); RectF rectF = sRectF.get(); if (rectF == null) { rectF = new RectF(); sRectF.set(rectF); } rectF.set(rect); m.mapRect(rectF); rect.set((int) (rectF.left + 0.5f), (int) (rectF.top + 0.5f), (int) (rectF.right + 0.5f), (int) (rectF.bottom + 0.5f)); } static void offsetDescendantMatrix(ViewParent target, View view, Matrix m) { final ViewParent parent = view.getParent(); if (parent instanceof View && parent != target) { final View vp = (View) parent; offsetDescendantMatrix(target, vp, m); m.preTranslate(-vp.getScrollX(), -vp.getScrollY()); } m.preTranslate(view.getLeft(), view.getTop()); if (!view.getMatrix().isIdentity()) { m.preConcat(view.getMatrix()); } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewOffsetHelper.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. */ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qmuiteam.qmui.util; import android.view.View; import androidx.core.view.ViewCompat; /** * Utility helper for moving a {@link View} around using * {@link View#offsetLeftAndRight(int)} and * {@link View#offsetTopAndBottom(int)}. *

* Also the setting of absolute offsets (similar to translationX/Y), rather than additive * offsets. */ public final class QMUIViewOffsetHelper { private final View mView; private int mLayoutTop; private int mLayoutLeft; private int mOffsetTop; private int mOffsetLeft; private boolean mVerticalOffsetEnabled = true; private boolean mHorizontalOffsetEnabled = true; public QMUIViewOffsetHelper(View view) { mView = view; } public void onViewLayout() { onViewLayout(true); } public void onViewLayout(boolean applyOffset) { mLayoutTop = mView.getTop(); mLayoutLeft = mView.getLeft(); if(applyOffset){ applyOffsets(); } } public void applyOffsets() { ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop)); ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft)); } /** * Set the top and bottom offset for this {@link QMUIViewOffsetHelper}'s view. * * @param offset the offset in px. * @return true if the offset has changed */ public boolean setTopAndBottomOffset(int offset) { if (mVerticalOffsetEnabled && mOffsetTop != offset) { mOffsetTop = offset; applyOffsets(); return true; } return false; } /** * Set the left and right offset for this {@link QMUIViewOffsetHelper}'s view. * * @param offset the offset in px. * @return true if the offset has changed */ public boolean setLeftAndRightOffset(int offset) { if (mHorizontalOffsetEnabled && mOffsetLeft != offset) { mOffsetLeft = offset; applyOffsets(); return true; } return false; } public boolean setOffset(int leftOffset, int topOffset) { if(!mHorizontalOffsetEnabled && !mVerticalOffsetEnabled){ return false; }else if(mHorizontalOffsetEnabled && mVerticalOffsetEnabled){ if (mOffsetLeft != leftOffset || mOffsetTop != topOffset) { mOffsetLeft = leftOffset; mOffsetTop = topOffset; applyOffsets(); return true; } return false; }else if(mHorizontalOffsetEnabled){ return setLeftAndRightOffset(leftOffset); }else{ return setTopAndBottomOffset(topOffset); } } public int getTopAndBottomOffset() { return mOffsetTop; } public int getLeftAndRightOffset() { return mOffsetLeft; } public int getLayoutTop() { return mLayoutTop; } public int getLayoutLeft() { return mLayoutLeft; } public void setHorizontalOffsetEnabled(boolean horizontalOffsetEnabled) { mHorizontalOffsetEnabled = horizontalOffsetEnabled; } public boolean isHorizontalOffsetEnabled() { return mHorizontalOffsetEnabled; } public void setVerticalOffsetEnabled(boolean verticalOffsetEnabled) { mVerticalOffsetEnabled = verticalOffsetEnabled; } public boolean isVerticalOffsetEnabled() { return mVerticalOffsetEnabled; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowHelper.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.util; import android.graphics.Rect; import android.os.Build; import android.view.View; import android.view.ViewParent; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.QMUIConfig; import java.lang.reflect.Field; /** * @author cginechen * @date 2016-08-05 */ public class QMUIWindowHelper { public static final int KEYBOARD_HEIGHT_BOUNDARY_DP = 100; /** * 设置WindowManager.LayoutParams的type *

* 1.使用 type 值为 TYPE_PHONE 和TYPE_SYSTEM_ALERT 需要申请 SYSTEM_ALERT_WINDOW 权限 * 2.type 值为 TYPE_TOAST 显示的 System overlay view 不需要权限,即可在任何平台显示。 * 3.type 值为 TYPE_TOAST在API level 19 以下因无法接收无法接收触摸(点击)和按键事件 * 4.Android 6.0 悬浮窗被默认被禁用,即使申请了 SYSTEM_ALERT_WINDOW 权限,应用也会crash,需要用户自己去开启 * (开启路径:通用 -- 应用管理 -- 更多 -- 配置应用 --- 在其他应用的上层显示 --- 选择你的APP -- 运行在其他应用的上层显示) * 5. 不直接返回type而是传layoutParams是不想调用者增加 @SuppressWarnings({"ResourceType"}) 跳过编译器的检查 */ public static void setWindowType(WindowManager.LayoutParams layoutParams) { layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST; } @Nullable @SuppressWarnings({"JavaReflectionMemberAccess"}) public static Rect unSafeGetWindowVisibleInsets(@NonNull View view) { Object attachInfo = getAttachInfoFromView(view); if (attachInfo == null) { return null; } try { // fortunately now it is in light greylist, just be warned. Field visibleInsetsField = attachInfo.getClass().getDeclaredField("mVisibleInsets"); visibleInsetsField.setAccessible(true); Object visibleInsets = visibleInsetsField.get(attachInfo); if (visibleInsets instanceof Rect) { return (Rect) visibleInsets; } } catch (Throwable e) { if (QMUIConfig.DEBUG) { e.printStackTrace(); } } return null; } @Nullable @SuppressWarnings({"JavaReflectionMemberAccess"}) public static Rect unSafeGetContentInsets(@NonNull View view) { Object attachInfo = getAttachInfoFromView(view); if (attachInfo == null) { return null; } try { // fortunately now it is in light greylist, just be warned. Field visibleInsetsField = attachInfo.getClass().getDeclaredField("mContentInsets"); visibleInsetsField.setAccessible(true); Object visibleInsets = visibleInsetsField.get(attachInfo); if (visibleInsets instanceof Rect) { return (Rect) visibleInsets; } } catch (Throwable e) { if (QMUIConfig.DEBUG) { e.printStackTrace(); } } return null; } public static Object getAttachInfoFromView(@NonNull View view) { Object attachInfo = null; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { // Android 10+ can not reflect the View.mAttachInfo // fortunately now it is in light greylist in ViewRootImpl View rootView = view.getRootView(); if (rootView != null) { ViewParent vp = rootView.getParent(); if (vp != null) { try { Field field = vp.getClass().getDeclaredField("mAttachInfo"); field.setAccessible(true); attachInfo = field.get(vp); } catch (Throwable e) { if (QMUIConfig.DEBUG) { e.printStackTrace(); } } } } } else { try { // Android P forbid the reflection for @hide filed, // fortunately now it is in light greylist, just be warned. Field field = View.class.getDeclaredField("mAttachInfo"); field.setAccessible(true); attachInfo = field.get(view); } catch (Throwable e) { if (QMUIConfig.DEBUG) { e.printStackTrace(); } } } return attachInfo; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/util/QMUIWindowInsetHelper.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.util; import android.os.Build; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.graphics.Insets; import androidx.core.view.OnApplyWindowInsetsListener; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.R; /** * @author cginechen * @date 2017-09-13 */ public class QMUIWindowInsetHelper { public final static InsetHandler consumeInsetWithPaddingHandler = new InsetHandler() { @Override public void handleInset(View view, Insets insets) { view.setPadding(insets.left, insets.top, insets.right, insets.bottom); } }; public final static InsetHandler consumeInsetWithPaddingIgnoreBottomHandler = new InsetHandler() { @Override public void handleInset(View view, Insets insets) { view.setPadding(insets.left, insets.top, insets.right, 0); } }; public final static InsetHandler consumeInsetWithPaddingIgnoreTopHandler = new InsetHandler() { @Override public void handleInset(View view, Insets insets) { view.setPadding(insets.left, 0, insets.right, insets.bottom); } }; public final static InsetHandler consumeInsetWithPaddingWithGravityHandler = new InsetHandler() { @Override public void handleInset(View view, Insets insets) { Insets toUsed = adapterInsetsWithGravity(view, insets); view.setPadding(toUsed.left, toUsed.top, toUsed.right, toUsed.bottom); } }; private final static OnApplyWindowInsetsListener sStopDispatchListener = new OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return WindowInsetsCompat.CONSUMED; } }; private final static OnApplyWindowInsetsListener sOverrideWithNothingHandleListener = new OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return insets; } }; public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType){ handleWindowInsets(v, insetsType, false); } public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, boolean jumpSelfHandleIfMatchLast){ handleWindowInsets(v, insetsType, jumpSelfHandleIfMatchLast, false); } public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, boolean jumpSelfHandleIfMatchLast, boolean ignoreVisibility){ handleWindowInsets(v, insetsType, consumeInsetWithPaddingWithGravityHandler, jumpSelfHandleIfMatchLast, ignoreVisibility, false); } public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, boolean jumpSelfHandleIfMatchLast, boolean ignoreVisibility, boolean stopDispatch){ handleWindowInsets(v, insetsType, consumeInsetWithPaddingWithGravityHandler, jumpSelfHandleIfMatchLast, ignoreVisibility, stopDispatch); } /** * * @param v the view to handle window insets. * @param insetsType the insets type * @param insetHandler insetHandler * @param jumpSelfHandleIfMatchLast if same as last, we do not dispatch window insets to v but return the last result directly. * @param stopDispatch it's dangerous to use this. if View.sBrokenInsetsDispatch is true, it will stop dispatching to siblings and children, * if View.sBrokenInsetsDispatch is false, it will only stop dispatching to children. But View.sBrokenInsetsDispatch is * not public. */ public static void handleWindowInsets(View v, @WindowInsetsCompat.Type.InsetsType final int insetsType, @NonNull final InsetHandler insetHandler, boolean jumpSelfHandleIfMatchLast, final boolean ignoreVisibility, final boolean stopDispatch ){ setOnApplyWindowInsetsListener(v, new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { if(v.getFitsSystemWindows()){ Insets toUsed = ignoreVisibility ? insets.getInsetsIgnoringVisibility(insetsType) : insets.getInsets(insetsType); insetHandler.handleInset(v, toUsed); if(stopDispatch){ return WindowInsetsCompat.CONSUMED; } } return insets; } }, jumpSelfHandleIfMatchLast); } /** * it's dangerous to use this. if View.sBrokenInsetsDispatch is true, it will stop dispatching to siblings and children, * if View.sBrokenInsetsDispatch is false, it will only stop dispatching to children. But View.sBrokenInsetsDispatch is * not public. * @param v the view to stop */ public static void stopDispatchWindowInsets(View v){ setOnApplyWindowInsetsListener(v, sStopDispatchListener, true); } public static void overrideWithDoNotHandleWindowInsets(View v){ setOnApplyWindowInsetsListener(v, sOverrideWithNothingHandleListener, false); } // copy from ViewCompat 1.5.0-beta01, fix the re dispatch problem. public static void setOnApplyWindowInsetsListener(final @NonNull View v, final @Nullable OnApplyWindowInsetsListener listener, final boolean reuseIfInputIsSame ) { // For backward compatibility of WindowInsetsAnimation, we use an // OnApplyWindowInsetsListener. We use the view tags to keep track of both listeners if (Build.VERSION.SDK_INT < 30) { v.setTag(R.id.tag_on_apply_window_listener, listener); } if (listener == null) { // If the listener is null, we need to make sure our compat listener, if any, is // set in-lieu of the listener being removed. View.OnApplyWindowInsetsListener compatInsetsAnimationCallback = (View.OnApplyWindowInsetsListener) v.getTag( R.id.tag_window_insets_animation_callback); v.setOnApplyWindowInsetsListener(compatInsetsAnimationCallback); return; } v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { WindowInsetsCompat mLastInsets = null; WindowInsets mReturnedInsets = null; @Override public WindowInsets onApplyWindowInsets(final View view, final WindowInsets insets) { WindowInsetsCompat compatInsets = WindowInsetsCompat.toWindowInsetsCompat( insets, view); // On API < 30, we request dispatch again until the input is same with last. boolean needRequestApplyInsetsAgain = true; if (Build.VERSION.SDK_INT < 30) { callCompatInsetAnimationCallback(insets, v); if (compatInsets.equals(mLastInsets)) { needRequestApplyInsetsAgain = false; if (reuseIfInputIsSame) { // We got the same insets we just return the previously computed insets. return mReturnedInsets; } } mLastInsets = compatInsets; } compatInsets = listener.onApplyWindowInsets(view, compatInsets); if (Build.VERSION.SDK_INT >= 30) { return compatInsets.toWindowInsets(); } // On API < 30, the visibleInsets, used to built WindowInsetsCompat, are // updated after the insets dispatch so we don't have the updated visible // insets at that point. As a workaround, we re-apply the insets so we know // that we'll have the right value the next time it's called. if(needRequestApplyInsetsAgain){ ViewCompat.requestApplyInsets(view); } // Keep a copy in case the insets haven't changed on the next call so we don't // need to call the listener again. mReturnedInsets = compatInsets.toWindowInsets(); return mReturnedInsets; } }); } /** * The backport of {@link WindowInsetsAnimationCompat.Callback} on API < 30 relies on * onApplyWindowInsetsListener, so if this callback is set, we'll call it in this method */ private static void callCompatInsetAnimationCallback(final @NonNull WindowInsets insets, final @NonNull View v) { // In case a WindowInsetsAnimationCompat.Callback is set, make sure to // call its compat listener. View.OnApplyWindowInsetsListener insetsAnimationCallback = (View.OnApplyWindowInsetsListener) v.getTag( R.id.tag_window_insets_animation_callback); if (insetsAnimationCallback != null) { insetsAnimationCallback.onApplyWindowInsets(v, insets); } } public static Insets adapterInsetsWithGravity(View view, Insets insets){ int left = insets.left; int right = insets.right; int top = insets.top; int bottom = insets.bottom; ViewGroup.LayoutParams lp = view.getLayoutParams(); if(lp instanceof ConstraintLayout.LayoutParams){ ConstraintLayout.LayoutParams constraintLp = (ConstraintLayout.LayoutParams) lp; if (constraintLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { if(constraintLp.leftToLeft == ConstraintLayout.LayoutParams.PARENT_ID){ right = 0; }else if(constraintLp.rightToRight == ConstraintLayout.LayoutParams.PARENT_ID){ left = 0; } } if (constraintLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { if(constraintLp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID){ bottom = 0; }else if(constraintLp.bottomToBottom == ConstraintLayout.LayoutParams.PARENT_ID){ top = 0; } } }else{ int gravity = -1; if (lp instanceof FrameLayout.LayoutParams) { gravity = ((FrameLayout.LayoutParams) lp).gravity; } /** * 因为该方法执行时机早于 FrameLayout.layoutChildren, * 而在 {FrameLayout#layoutChildren} 中当 gravity == -1 时会设置默认值为 Gravity.TOP | Gravity.LEFT, * 所以这里也要同样设置 */ if (gravity == -1) { gravity = Gravity.TOP | Gravity.LEFT; } if (lp.width != ViewGroup.LayoutParams.MATCH_PARENT) { int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; switch (horizontalGravity) { case Gravity.LEFT: right = 0; break; case Gravity.RIGHT: left = 0; break; } } if (lp.height != ViewGroup.LayoutParams.MATCH_PARENT) { int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (verticalGravity) { case Gravity.TOP: bottom = 0; break; case Gravity.BOTTOM: top = 0; break; } } } return Insets.of(left, top, right, bottom); } public interface InsetHandler{ void handleInset(View view, Insets insets); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/IBlankTouchDetector.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.widget; import android.view.MotionEvent; public interface IBlankTouchDetector { boolean isTouchInBlank(MotionEvent event); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/IWindowInsetKeyboardConsumer.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.widget; public interface IWindowInsetKeyboardConsumer { void onHandleKeyboard(int keyboardInset); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAnimationListView.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.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Canvas; import android.os.Looper; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.widget.BaseAdapter; import android.widget.ListAdapter; import android.widget.ListView; import androidx.collection.LongSparseArray; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.QMUILog; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * 使 {@link ListView} 支持添加/删除 Item 的动画,支持自定义动画效果。 *

* https://github.com/cypressious/AnimationListView/blob/master/AnimationListView/src/de/cypressworks/animationlistview/AnimationListView.java *

* 一个痛点: * 在LayoutTransition中有一个CHANGE_DISAPPEAR的概念:指由于添加或移动等操作导致子view消失的场景。 * QMUIAnimationListView同样有这样的场景,但是ListView上LayoutTransition不生效,所以我们需要自己模拟实现。 * 但是当layout后,消失的view已经被回收了,我们只能对 ListView 当前存在的view做动画,那么如何去做CHANGE_DISAPPEAR动画呢? * 这里采用了一个非常挫的实现方案(暂时没想到其它好的方案): * 1.在layout前对当前屏幕的view都设置 setHasTransientState(true)。这样做后,view不会被直接被ListView回收到scrap中 * 2.将当前屏幕的view都保存在mDetachViewsMap中 * 3.在draw之前(这个时候我们已经能确定哪些item会完全离开屏幕了),剔除不会完全离开屏幕的item * 4.开启一个ValueAnimator,每次update时调用invalidate()触发 onDraw() 方法 * 5.在 onDraw() 方法中根据animator的已动画时间计算view动画的位置,调用view.draw方法draw出来,因为是之前存在过的view,大小必定是确定的 * 6.最后需要调用 setHasTransientState(false),以便view最终可以被回收 *

* 这种方法的代价就是:当前屏幕的item不能够被及时回收(最终还是会被回收的) * 所以增加 mOpenChangeDisappearAnimation 变量,如果你并不在意 CHANGE_DISAPPEAR 没有动画的那一点点不协调,那就不用开启它 * * @author cginechen * @date 2017-03-30 */ @SuppressWarnings({"rawtypes", "unchecked"}) public class QMUIAnimationListView extends ListView { private static final String TAG = "QMUIAnimationListView"; private static final long DURATION_ALPHA = 300; private static final long DURATION_OFFSET_MIN = 0; private static final long DURATION_OFFSET_MAX = 1000; private static final float DEFAULT_OFFSET_DURATION_UNIT = 0.5f; protected final LongSparseArray mTopMap = new LongSparseArray<>(); protected final LongSparseArray mPositionMap = new LongSparseArray<>(); protected final LongSparseArray mDetachViewsMap = new LongSparseArray<>(); protected final Set mBeforeVisible = new HashSet<>(); protected final Set mAfterVisible = new HashSet<>(); private final List mPendingManipulations = new ArrayList<>(); private final List mPendingManipulationsWithoutAnimation = new ArrayList<>(); private long mChangeDisappearPlayTime = 0; private ValueAnimator mChangeDisappearAnimator; private ListAdapter mRealAdapter; private WrapperAdapter mWrapperAdapter; private boolean mIsAnimating = false; private int mAnimationManipulateDurationLimit = 0; private long mLastManipulateTime = 0; private float mOffsetDurationUnit = DEFAULT_OFFSET_DURATION_UNIT; // 移动1px的时间 private Interpolator mOffsetInterpolator = new LinearInterpolator(); private boolean mOpenChangeDisappearAnimation = false; public QMUIAnimationListView(Context context) { this(context, null); } public QMUIAnimationListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public QMUIAnimationListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public QMUIAnimationListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } private void init() { setWillNotDraw(false); } public ListAdapter getRealAdapter() { return mRealAdapter; } @Override public void setAdapter(ListAdapter adapter) { mRealAdapter = adapter; mWrapperAdapter = adapter != null ? new WrapperAdapter(mRealAdapter) : null; super.setAdapter(mWrapperAdapter); } public void setAnimationManipulateDurationLimit(int animationManipulateDurationLimit) { mAnimationManipulateDurationLimit = animationManipulateDurationLimit; } public void manipulate(final Manipulator manipulator) { Log.i(TAG, "manipulate"); if (!mWrapperAdapter.isAnimationEnabled()) { manipulateWithoutAnimation(manipulator); return; } long now = SystemClock.uptimeMillis(); boolean notLimitedAnimation = now - mLastManipulateTime > mAnimationManipulateDurationLimit; mLastManipulateTime = now; if (!mIsAnimating) { if (notLimitedAnimation) { mIsAnimating = true; prepareAnimation(); manipulator.manipulate((T) mRealAdapter); doAnimation(); } else { manipulator.manipulate((T) mRealAdapter); mWrapperAdapter.notifyDataSetChanged(); } } else { if (notLimitedAnimation) { mPendingManipulations.add(manipulator); } else { mPendingManipulationsWithoutAnimation.add(manipulator); } } } public void manipulateWithoutAnimation(final Manipulator manipulator) { Log.i(TAG, "manipulateWithoutAnimation"); if (!mIsAnimating) { manipulator.manipulate((T) mRealAdapter); mWrapperAdapter.notifyDataSetChanged(); } else { mPendingManipulationsWithoutAnimation.add(manipulator); } } public float getOffsetDurationUnit() { return mOffsetDurationUnit; } public void setOffsetDurationUnit(float offsetDurationUnit) { mOffsetDurationUnit = offsetDurationUnit; } private long getOffsetDuration(int start, int end) { long duration = (long) (Math.abs(start - end) * mOffsetDurationUnit); return Math.max(DURATION_OFFSET_MIN, Math.min(duration, DURATION_OFFSET_MAX)); } /** * 是否启用 CHANGE-DISAPPEAR 动画。 * * @param openChangeDisappearAnimation true 为启用 CHANGE-DISAPPEAR 动画,false 则不启用。 */ public void setOpenChangeDisappearAnimation(boolean openChangeDisappearAnimation) { mOpenChangeDisappearAnimation = openChangeDisappearAnimation; } public void setOffsetInterpolator(Interpolator offsetInterpolator) { mOffsetInterpolator = offsetInterpolator; } private void prepareAnimation() { mTopMap.clear(); mPositionMap.clear(); mBeforeVisible.clear(); mAfterVisible.clear(); mDetachViewsMap.clear(); mWrapperAdapter.setShouldNotifyChange(false); int childCount = getChildCount(); int firstVisiblePos = getFirstVisiblePosition(); for (int i = 0; i < childCount; i++) { final View view = getChildAt(i); long id = mWrapperAdapter.getItemId(firstVisiblePos + i); mTopMap.put(id, view.getTop()); mPositionMap.put(id, i); } for (int i = 0; i < firstVisiblePos; i++) { final long id = mWrapperAdapter.getItemId(i); mBeforeVisible.add(id); } final int count = mWrapperAdapter.getCount(); for (int i = firstVisiblePos + childCount; i < count; i++) { final long id = mWrapperAdapter.getItemId(i); mAfterVisible.add(id); } } private void doAnimation() { setEnabled(false); setClickable(false); doPreLayoutAnimation(new ManipulateAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { mWrapperAdapter.notifyDataSetChanged(); getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); doPostLayoutAnimation(); return true; } }); } }); } private void doPreLayoutAnimation(Animator.AnimatorListener listener) { final AnimatorSet animatorSet = new AnimatorSet(); ArrayList deleteIds = new ArrayList<>(); int i; for (i = 0; i < mTopMap.size(); i++) { long id = mTopMap.keyAt(i); int newPos = getPositionForId(id); if (newPos < 0) { // delete int oldPos = mPositionMap.get(id); View child = getChildAt(oldPos); final Animator anim = getDeleteAnimator(child); mPositionMap.remove(id); animatorSet.play(anim); deleteIds.add(id); } } for (i = 0; i < deleteIds.size(); i++) { mTopMap.remove(deleteIds.get(i)); } if (mOpenChangeDisappearAnimation) { for (i = 0; i < mPositionMap.size(); i++) { View view = getChildAt(mPositionMap.valueAt(i)); ViewCompat.setHasTransientState(view, true); mDetachViewsMap.put(mPositionMap.keyAt(i), view); } } if (!animatorSet.getChildAnimations().isEmpty()) { animatorSet.addListener(listener); animatorSet.start(); } else { listener.onAnimationEnd(animatorSet); } } private void doPostLayoutAnimation() { final AnimatorSet animatorSet = new AnimatorSet(); int childCount = getChildCount(); int firstVisiblePos = getFirstVisiblePosition(); Animator anim = null; int addOccurTop = -1; int addOccurPosition = -1; if (mOpenChangeDisappearAnimation) { for (int i = 0; i < mDetachViewsMap.size(); i++) { ViewCompat.setHasTransientState(mDetachViewsMap.valueAt(i), false); } } for (int i = 0; i < childCount; i++) { View child = getChildAt(i); child.setAlpha(1f); int newTop = child.getTop(); int position = firstVisiblePos + i; long id = mWrapperAdapter.getItemId(position); if (mTopMap.indexOfKey(id) > -1) { addOccurTop = -1; int oldTop = mTopMap.get(id); mTopMap.remove(id); mPositionMap.remove(id); if (mOpenChangeDisappearAnimation) { mDetachViewsMap.remove(id); } if (oldTop != newTop) { anim = getOffsetAnimator(child, oldTop, newTop); } } else if (mBeforeVisible.contains(id)) { addOccurTop = -1; int oldTop = -child.getHeight(); anim = getOffsetAnimator(child, oldTop, newTop); } else if (mAfterVisible.contains(id)) { addOccurTop = -1; int oldTop = getHeight(); anim = getOffsetAnimator(child, oldTop, newTop); } else { // new add item if (addOccurTop == -1) { addOccurTop = newTop; addOccurPosition = position; } anim = getAddAnimator(child, newTop, position, addOccurTop, addOccurPosition); } if (anim != null) { animatorSet.play(anim); } } if (mOpenChangeDisappearAnimation && mDetachViewsMap.size() > 0) { mChangeDisappearAnimator = ValueAnimator.ofFloat(0, 1); mChangeDisappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mChangeDisappearPlayTime = animation.getCurrentPlayTime(); invalidate(); } }); mChangeDisappearAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mChangeDisappearPlayTime = 0; } }); mChangeDisappearAnimator.setDuration(getChangeDisappearDuration()); mChangeDisappearAnimator.start(); } animatorSet.addListener(new ManipulateAnimatorListener() { @Override public void onAnimationEnd(final Animator animation) { finishAnimation(); } }); animatorSet.start(); } protected long getChangeDisappearDuration() { return (long) (getHeight() * mOffsetDurationUnit); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mOpenChangeDisappearAnimation && mChangeDisappearAnimator != null && mChangeDisappearAnimator.isStarted() && mDetachViewsMap.size() > 0 && mIsAnimating) { for (int i = 0; i < mDetachViewsMap.size(); i++) { long id = mDetachViewsMap.keyAt(i); View view = mDetachViewsMap.valueAt(i); int newPos = getPositionForId(id); int top, offset = (int) (mChangeDisappearPlayTime / mOffsetDurationUnit); if (newPos < getFirstVisiblePosition()) { top = mTopMap.get(id) - offset; if (top < -view.getHeight()) { continue; } } else { top = mTopMap.get(id) + offset; if (top > getHeight()) { continue; } } view.layout(0, top, view.getWidth(), top + view.getHeight()); view.setAlpha(1f - mChangeDisappearPlayTime * 1f / getChangeDisappearDuration()); // 不能直接调用view.draw(canvas), 在listview上由于缓存会冲突 drawChild(canvas, view, getDrawingTime()); } } } private void finishAnimation() { mWrapperAdapter.setShouldNotifyChange(true); mChangeDisappearAnimator = null; if (mOpenChangeDisappearAnimation) { for (int i = 0; i < mDetachViewsMap.size(); i++) { mDetachViewsMap.valueAt(i).setAlpha(1); } mDetachViewsMap.clear(); } mIsAnimating = false; setEnabled(true); setClickable(true); manipulatePending(); } private void manipulatePending() { if (!mPendingManipulationsWithoutAnimation.isEmpty()) { mIsAnimating = true; for (final Manipulator manipulator : mPendingManipulationsWithoutAnimation) { manipulator.manipulate(mRealAdapter); } mPendingManipulationsWithoutAnimation.clear(); mWrapperAdapter.notifyDataSetChanged(); post(new Runnable() { @Override public void run() { mIsAnimating = false; manipulatePending(); } }); } else { if (mPendingManipulations.isEmpty()) { return; } mIsAnimating = true; prepareAnimation(); for (final Manipulator manipulator : mPendingManipulations) { manipulator.manipulate(mRealAdapter); } mPendingManipulations.clear(); doAnimation(); } } protected Animator getDeleteAnimator(View view) { return alphaObjectAnimator(view, false, DURATION_ALPHA, true); } protected Animator getOffsetAnimator(View view, int oldTop, int newTop) { return getOffsetAnimator(view, oldTop, newTop, getOffsetDuration(oldTop, newTop)); } protected Animator getOffsetAnimator(View view, int oldTop, int newTop, long duration) { final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "translationY", oldTop - newTop, 0); anim.setDuration(duration); anim.setInterpolator(mOffsetInterpolator); return anim; } protected Animator getAddAnimator(View view, int top, int position, int addOccurTop, int addOccurPosition) { view.setAlpha(0); view.clearAnimation(); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(alphaObjectAnimator(view, true, 50, false)); if (addOccurTop != top) { animatorSet.play(getOffsetAnimator(view, addOccurTop, top)); } animatorSet.setStartDelay((long) (view.getHeight() * mOffsetDurationUnit)); return animatorSet; } protected ObjectAnimator alphaObjectAnimator(View view, final boolean fadeIn, long duration, boolean postBack) { final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", fadeIn ? new float[]{ 0f, 1f} : new float[]{1f, 0f}); anim.setDuration(duration); if (postBack) { final WeakReference wr = new WeakReference<>(view); anim.addListener(new ManipulateAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { if (wr.get() != null) { wr.get().setAlpha(fadeIn ? 0 : 1); } } }); } return anim; } protected int getPositionForId(final long id) { for (int i = 0; i < mWrapperAdapter.getCount(); i++) { if (mWrapperAdapter.getItemId(i) == id) { return i; } } return -1; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return isEnabled() && super.dispatchTouchEvent(ev); } public interface Manipulator { void manipulate(T adapter); } private static class WrapperAdapter extends BaseAdapter { private ListAdapter mAdapter; private boolean mShouldNotifyChange = true; private final DataSetObserver mObserver = new DataSetObserver() { @Override public void onChanged() { if (mShouldNotifyChange) { notifyDataSetChanged(); } } @Override public void onInvalidated() { notifyDataSetInvalidated(); } }; private boolean mIsAnimationEnabled = false; public WrapperAdapter(ListAdapter adapter) { mAdapter = adapter; mAdapter.registerDataSetObserver(mObserver); } public void setShouldNotifyChange(boolean shouldNotifyChange) { mShouldNotifyChange = shouldNotifyChange; } public boolean isAnimationEnabled() { return mIsAnimationEnabled; } @Override public void notifyDataSetChanged() { if (Looper.myLooper() != Looper.getMainLooper()) { QMUILog.d(TAG, "notifyDataSetChanged not in main Thread"); return; } super.notifyDataSetChanged(); } @Override public int getCount() { return mAdapter.getCount(); } @Override public int getItemViewType(int position) { return mAdapter.getItemViewType(position); } @Override public int getViewTypeCount() { return mAdapter.getViewTypeCount(); } @Override public Object getItem(int position) { return mAdapter.getItem(position); } @Override public long getItemId(int position) { return mAdapter.getItemId(position); } @Override public View getView(int position, View convertView, ViewGroup parent) { return mAdapter.getView(position, convertView, parent); } @Override public boolean hasStableIds() { boolean stable = mAdapter.hasStableIds(); mIsAnimationEnabled = stable; return stable; } } private abstract class ManipulateAnimatorListener implements Animator.AnimatorListener { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIAppBarLayout.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.widget; import android.content.Context; import android.util.AttributeSet; import com.google.android.material.appbar.AppBarLayout; @Deprecated public class QMUIAppBarLayout extends AppBarLayout { public QMUIAppBarLayout(Context context) { super(context); } public QMUIAppBarLayout(Context context, AttributeSet attrs) { super(context, attrs); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUICollapsingTopBarLayout.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. */ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qmuiteam.qmui.widget; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.WindowInsets; import android.widget.FrameLayout; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.StyleRes; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; import com.qmuiteam.qmui.QMUIInterpolatorStaticHolder; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.IQMUISkinDispatchInterceptor; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.util.QMUICollapsingTextHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Objects; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; /** * 参考 {@link CollapsingToolbarLayout}, 适配 QMUITopBar * * @author cginechen * @date 2017-09-02 */ public class QMUICollapsingTopBarLayout extends FrameLayout implements IQMUISkinDispatchInterceptor { private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600; private boolean mRefreshToolbar = true; private int mTopBarId; private QMUITopBar mTopBar; private View mTopBarDirectChild; private int mExpandedMarginStart; private int mExpandedMarginTop; private int mExpandedMarginEnd; private int mExpandedMarginBottom; private final Rect mTmpRect = new Rect(); final QMUICollapsingTextHelper mCollapsingTextHelper; private boolean mCollapsingTitleEnabled; private Drawable mContentScrim; Drawable mStatusBarScrim; private int mScrimAlpha; private boolean mScrimsAreShown; private ValueAnimator mScrimAnimator; private long mScrimAnimationDuration; private int mScrimVisibleHeightTrigger = -1; private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; private ValueAnimator.AnimatorUpdateListener mScrimUpdateListener; private ArrayList mOnOffsetUpdateListeners = new ArrayList<>(); int mCurrentOffset; Insets mLastInsets; private int mContentScrimSkinAttr = 0; private int mStatusBarScrimSkinAttr = 0; private int mCollapsedTextColorSkinAttr = 0; private int mExpandedTextColorSkinAttr = 0; public QMUICollapsingTopBarLayout(Context context) { this(context, null); } public QMUICollapsingTopBarLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUICollapsingTopBarLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mCollapsingTextHelper = new QMUICollapsingTextHelper(this); mCollapsingTextHelper.setTextSizeInterpolator(QMUIInterpolatorStaticHolder.DECELERATE_INTERPOLATOR); QMUIViewHelper.checkAppCompatTheme(context); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.QMUICollapsingTopBarLayout, defStyleAttr, 0); mCollapsingTextHelper.setExpandedTextGravity( a.getInt(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleGravity, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)); mCollapsingTextHelper.setCollapsedTextGravity( a.getInt(R.styleable.QMUICollapsingTopBarLayout_qmui_collapsedTitleGravity, GravityCompat.START | Gravity.CENTER_VERTICAL)); mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom = a.getDimensionPixelSize(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMargin, 0); if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginStart)) { mExpandedMarginStart = a.getDimensionPixelSize( R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginStart, 0); } if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginEnd)) { mExpandedMarginEnd = a.getDimensionPixelSize( R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginEnd, 0); } if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginTop)) { mExpandedMarginTop = a.getDimensionPixelSize( R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginTop, 0); } if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginBottom)) { mExpandedMarginBottom = a.getDimensionPixelSize( R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleMarginBottom, 0); } mCollapsingTitleEnabled = a.getBoolean(R.styleable.QMUICollapsingTopBarLayout_qmui_titleEnabled, true); setTitle(a.getText(R.styleable.QMUICollapsingTopBarLayout_qmui_title)); // First load the default text appearances mCollapsingTextHelper.setExpandedTextAppearance(R.style.QMUI_CollapsingTopBarLayoutExpanded); mCollapsingTextHelper.setCollapsedTextAppearance(R.style.QMUI_CollapsingTopBarLayoutCollapsed); // Now overlay any custom text appearances if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleTextAppearance)) { mCollapsingTextHelper.setExpandedTextAppearance( a.getResourceId(R.styleable.QMUICollapsingTopBarLayout_qmui_expandedTitleTextAppearance, 0)); } if (a.hasValue(R.styleable.QMUICollapsingTopBarLayout_qmui_collapsedTitleTextAppearance)) { mCollapsingTextHelper.setCollapsedTextAppearance( a.getResourceId(R.styleable.QMUICollapsingTopBarLayout_qmui_collapsedTitleTextAppearance, 0)); } mScrimVisibleHeightTrigger = a.getDimensionPixelSize( R.styleable.QMUICollapsingTopBarLayout_qmui_scrimVisibleHeightTrigger, -1); mScrimAnimationDuration = a.getInt( R.styleable.QMUICollapsingTopBarLayout_qmui_scrimAnimationDuration, DEFAULT_SCRIM_ANIMATION_DURATION); mTopBarId = a.getResourceId(R.styleable.QMUICollapsingTopBarLayout_qmui_topBarId, -1); if (a.getBoolean(R.styleable.QMUICollapsingTopBarLayout_qmui_followTopBarCommonSkin, false)) { followTopBarCommonSkin(); } else { setContentScrimInner(a.getDrawable(R.styleable.QMUICollapsingTopBarLayout_qmui_contentScrim)); setStatusBarScrimInner(a.getDrawable(R.styleable.QMUICollapsingTopBarLayout_qmui_statusBarScrim)); } a.recycle(); setWillNotDraw(false); QMUIWindowInsetHelper.handleWindowInsets(this, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout(), new QMUIWindowInsetHelper.InsetHandler() { @Override public void handleInset(View view, Insets insets) { Insets newInsets = null; if (ViewCompat.getFitsSystemWindows(view)) { // If we're set to fit system windows, keep the insets newInsets = insets; } // If our insets have changed, keep them and invalidate the scroll ranges... if (!Objects.equals(mLastInsets, insets)) { mLastInsets = newInsets; requestLayout(); } } }, true, false, true ); } public void followTopBarCommonSkin() { setCollapsedTextColorSkinAttr(R.attr.qmui_skin_support_topbar_title_color); setExpandedTextColorSkinAttr(R.attr.qmui_skin_support_topbar_title_color); setContentScrimSkinAttr(R.attr.qmui_skin_support_topbar_bg); setStatusBarScrimSkinAttr(R.attr.qmui_skin_support_topbar_bg); } @Override public void onViewAdded(View child) { super.onViewAdded(child); if (child instanceof QMUITopBar) { ((QMUITopBar) child).disableBackgroundSetter(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // Add an OnOffsetChangedListener if possible final ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { // Copy over from the ABL whether we should fit system windows ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent)); if (mOnOffsetChangedListener == null) { mOnOffsetChangedListener = new OffsetUpdateListener(); } ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener); // We're attached, so lets request an inset dispatch ViewCompat.requestApplyInsets(this); } } @Override protected void onDetachedFromWindow() { // Remove our OnOffsetChangedListener if possible and it exists final ViewParent parent = getParent(); if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) { ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener); } super.onDetachedFromWindow(); } @Override public void draw(Canvas canvas) { super.draw(canvas); // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. // Instead, we draw it here, before our collapsing text. ensureToolbar(); if (mTopBar == null && mContentScrim != null && mScrimAlpha > 0) { mContentScrim.mutate().setAlpha(mScrimAlpha); mContentScrim.draw(canvas); } // Let the collapsing text helper draw its text if (mCollapsingTitleEnabled) { mCollapsingTextHelper.draw(canvas); } // Now draw the status bar scrim if (mStatusBarScrim != null && mScrimAlpha > 0) { final int topInset = getWindowInsetTop(); if (topInset > 0) { mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(), topInset - mCurrentOffset); mStatusBarScrim.mutate().setAlpha(mScrimAlpha); mStatusBarScrim.draw(canvas); } } } private int getWindowInsetTop() { if (mLastInsets != null) { return mLastInsets.top; } return 0; } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present), // but in front of any other children which are behind it. To do this we intercept the // drawChild() call, and draw our scrim just before the Toolbar is drawn boolean invalidated = false; if (mContentScrim != null && mScrimAlpha > 0 && isToolbarChild(child)) { mContentScrim.mutate().setAlpha(mScrimAlpha); mContentScrim.draw(canvas); invalidated = true; } return super.drawChild(canvas, child, drawingTime) || invalidated; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mContentScrim != null) { mContentScrim.setBounds(0, 0, w, h); } } private void ensureToolbar() { if (!mRefreshToolbar) { return; } // First clear out the current Toolbar mTopBar = null; mTopBarDirectChild = null; if (mTopBarId != -1) { // If we have an ID set, try and find it and it's direct parent to us mTopBar = (QMUITopBar) findViewById(mTopBarId); if (mTopBar != null) { mTopBarDirectChild = findDirectChild(mTopBar); } } if (mTopBar == null) { // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find // one from our direct children QMUITopBar topBar = null; for (int i = 0, count = getChildCount(); i < count; i++) { final View child = getChildAt(i); if (child instanceof QMUITopBar) { topBar = (QMUITopBar) child; break; } } mTopBar = topBar; } mRefreshToolbar = false; } private boolean isToolbarChild(View child) { return (mTopBarDirectChild == null || mTopBarDirectChild == this) ? child == mTopBar : child == mTopBarDirectChild; } /** * Returns the direct child of this layout, which itself is the ancestor of the * given view. */ private View findDirectChild(final View descendant) { View directChild = descendant; for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) { if (p instanceof View) { directChild = (View) p; } } return directChild; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ensureToolbar(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { super.dispatchApplyWindowInsets(insets); // stop dispatch, but prevent stop parent sibling. return insets; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mLastInsets != null) { // Shift down any views which are not set to fit system windows final int insetTop = getWindowInsetTop(); for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (ViewCompat.getFitsSystemWindows(child)) { if (child.getTop() < insetTop) { // If the child isn't set to fit system windows but is drawing within // the inset offset it down ViewCompat.offsetTopAndBottom(child, insetTop); } } } } for (int i = 0, z = getChildCount(); i < z; i++) { getViewOffsetHelper(getChildAt(i)).onViewLayout(false); } // Update the collapsed bounds by getting it's transformed bounds if (mCollapsingTitleEnabled) { // Update the collapsed bounds final int maxOffset = getMaxOffsetForPinChild( mTopBarDirectChild != null ? mTopBarDirectChild : mTopBar); QMUIViewHelper.getDescendantRect(this, mTopBar, mTmpRect); // mTmpRect.top = mTmpRect.top - topBarInsetAdjustTop; Rect rect = mTopBar.getTitleContainerRect(); mCollapsingTextHelper.setCollapsedBounds( mTmpRect.left + rect.left, mTmpRect.top + maxOffset + rect.top, mTmpRect.left + rect.right, mTmpRect.top + maxOffset + rect.bottom); // Update the expanded bounds mCollapsingTextHelper.setExpandedBounds( mExpandedMarginStart, mTmpRect.top + mExpandedMarginTop, right - left - mExpandedMarginEnd, bottom - top - mExpandedMarginBottom); // Now recalculate using the new bounds mCollapsingTextHelper.recalculate(); } // Finally, set our minimum height to enable proper AppBarLayout collapsing if (mTopBar != null) { if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) { // If we do not currently have a title, try and grab it from the Toolbar mCollapsingTextHelper.setText(mTopBar.getTitle()); } if (mTopBarDirectChild == null || mTopBarDirectChild == this) { setMinimumHeight(getHeightWithMargins(mTopBar)); } else { setMinimumHeight(getHeightWithMargins(mTopBarDirectChild)); } } updateScrimVisibility(); for (int i = 0, z = getChildCount(); i < z; i++) { getViewOffsetHelper(getChildAt(i)).applyOffsets(); } } private static int getHeightWithMargins(@NonNull final View view) { final ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof MarginLayoutParams) { final MarginLayoutParams mlp = (MarginLayoutParams) lp; return view.getHeight() + mlp.topMargin + mlp.bottomMargin; } return view.getHeight(); } static QMUIViewOffsetHelper getViewOffsetHelper(View view) { QMUIViewOffsetHelper offsetHelper = (QMUIViewOffsetHelper) view.getTag(R.id.qmui_view_offset_helper); if (offsetHelper == null) { offsetHelper = new QMUIViewOffsetHelper(view); view.setTag(R.id.qmui_view_offset_helper, offsetHelper); } return offsetHelper; } /** * Sets the title to be displayed by this view, if enabled. * * @see #setTitleEnabled(boolean) * @see #getTitle() */ public void setTitle(@Nullable CharSequence title) { mCollapsingTextHelper.setText(title); } /** * Returns the title currently being displayed by this view. If the title is not enabled, then * this will return {@code null}. */ @Nullable public CharSequence getTitle() { return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null; } /** * Sets whether this view should display its own title. *

*

The title displayed by this view will shrink and grow based on the scroll offset.

* * @see #setTitle(CharSequence) * @see #isTitleEnabled() */ public void setTitleEnabled(boolean enabled) { if (enabled != mCollapsingTitleEnabled) { mCollapsingTitleEnabled = enabled; requestLayout(); } } /** * Returns whether this view is currently displaying its own title. * * @see #setTitleEnabled(boolean) */ public boolean isTitleEnabled() { return mCollapsingTitleEnabled; } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change * in the vertical scroll may overwrite this value. Any visibility change will be animated if * this view has already been laid out. * * @param shown whether the scrims should be shown * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown) { setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode()); } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change * in the vertical scroll may overwrite this value. * * @param shown whether the scrims should be shown * @param animate whether to animate the visibility change * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown, boolean animate) { if (mScrimsAreShown != shown) { if (animate) { animateScrim(shown ? 0xFF : 0x0); } else { setScrimAlpha(shown ? 0xFF : 0x0); } mScrimsAreShown = shown; } } /** * @param scrimUpdateListener 为 null 则是 removeUpdateListener */ public void setScrimUpdateListener(ValueAnimator.AnimatorUpdateListener scrimUpdateListener) { if (mScrimUpdateListener != scrimUpdateListener) { if (mScrimAnimator == null) { mScrimUpdateListener = scrimUpdateListener; } else { if (mScrimUpdateListener != null) { mScrimAnimator.removeUpdateListener(mScrimUpdateListener); } mScrimUpdateListener = scrimUpdateListener; if (mScrimUpdateListener != null) { mScrimAnimator.addUpdateListener(mScrimUpdateListener); } } } } private void animateScrim(int targetAlpha) { ensureToolbar(); if (mScrimAnimator == null) { mScrimAnimator = new ValueAnimator(); mScrimAnimator.setDuration(mScrimAnimationDuration); mScrimAnimator.setInterpolator( targetAlpha > mScrimAlpha ? QMUIInterpolatorStaticHolder.FAST_OUT_LINEAR_IN_INTERPOLATOR : QMUIInterpolatorStaticHolder.LINEAR_OUT_SLOW_IN_INTERPOLATOR); mScrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { setScrimAlpha((Integer) animator.getAnimatedValue()); } }); if (mScrimUpdateListener != null) { mScrimAnimator.addUpdateListener(mScrimUpdateListener); } } else if (mScrimAnimator.isRunning()) { mScrimAnimator.cancel(); } mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha); mScrimAnimator.start(); } void setScrimAlpha(int alpha) { if (alpha != mScrimAlpha) { final Drawable contentScrim = mContentScrim; if (contentScrim != null && mTopBar != null) { ViewCompat.postInvalidateOnAnimation(mTopBar); } mScrimAlpha = alpha; ViewCompat.postInvalidateOnAnimation(QMUICollapsingTopBarLayout.this); } } int getScrimAlpha() { return mScrimAlpha; } public void setContentScrimSkinAttr(int contentScrimSkinAttr) { mContentScrimSkinAttr = contentScrimSkinAttr; if (contentScrimSkinAttr != 0) { setStatusBarScrimInner(QMUISkinHelper.getSkinDrawable(this, contentScrimSkinAttr)); } } /** * Set the drawable to use for the content scrim from resources. Providing null will disable * the scrim functionality. * * @param drawable the drawable to display * @see #getContentScrim() */ public void setContentScrim(@Nullable Drawable drawable) { mContentScrimSkinAttr = 0; setContentScrimInner(drawable); } private void setContentScrimInner(@Nullable Drawable drawable) { if (mContentScrim != drawable) { if (mContentScrim != null) { mContentScrim.setCallback(null); } mContentScrim = drawable != null ? drawable.mutate() : null; if (mContentScrim != null) { mContentScrim.setBounds(0, 0, getWidth(), getHeight()); mContentScrim.setCallback(this); mContentScrim.setAlpha(mScrimAlpha); } ViewCompat.postInvalidateOnAnimation(this); } } /** * Set the color to use for the content scrim. * * @param color the color to display * @see #getContentScrim() */ public void setContentScrimColor(@ColorInt int color) { setContentScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @see #getContentScrim() */ public void setContentScrimResource(@DrawableRes int resId) { setContentScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the foreground scrim. * * @see #setContentScrim(Drawable) */ @Nullable public Drawable getContentScrim() { return mContentScrim; } /** * Set the drawable to use for the status bar scrim from resources. * Providing null will disable the scrim functionality. *

*

This scrim is only shown when we have been given a top system inset.

* * @param drawable the drawable to display * @see #getStatusBarScrim() */ public void setStatusBarScrim(@Nullable Drawable drawable) { mStatusBarScrimSkinAttr = 0; setStatusBarScrimInner(drawable); } private void setStatusBarScrimInner(@Nullable Drawable drawable) { if (mStatusBarScrim != drawable) { if (mStatusBarScrim != null) { mStatusBarScrim.setCallback(null); } mStatusBarScrim = drawable != null ? drawable.mutate() : null; if (mStatusBarScrim != null) { if (mStatusBarScrim.isStateful()) { mStatusBarScrim.setState(getDrawableState()); } DrawableCompat.setLayoutDirection(mStatusBarScrim, ViewCompat.getLayoutDirection(this)); mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false); mStatusBarScrim.setCallback(this); mStatusBarScrim.setAlpha(mScrimAlpha); } ViewCompat.postInvalidateOnAnimation(this); } } public void setStatusBarScrimSkinAttr(int statusBarScrimSkinAttr) { mStatusBarScrimSkinAttr = statusBarScrimSkinAttr; if (mStatusBarScrimSkinAttr != 0) { setStatusBarScrimInner(QMUISkinHelper.getSkinDrawable(this, statusBarScrimSkinAttr)); } } // 从系统源码获取,不作检测 @SuppressWarnings("ConstantConditions") @Override protected void drawableStateChanged() { super.drawableStateChanged(); final int[] state = getDrawableState(); boolean changed = false; Drawable d = mStatusBarScrim; if (d != null && d.isStateful()) { changed |= d.setState(state); } d = mContentScrim; if (d != null && d.isStateful()) { changed |= d.setState(state); } if (mCollapsingTextHelper != null) { changed |= mCollapsingTextHelper.setState(state); } if (changed) { invalidate(); } } @Override protected boolean verifyDrawable(@NonNull Drawable who) { return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim; } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); final boolean visible = visibility == VISIBLE; if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) { mStatusBarScrim.setVisible(visible, false); } if (mContentScrim != null && mContentScrim.isVisible() != visible) { mContentScrim.setVisible(visible, false); } } /** * Set the color to use for the status bar scrim. *

*

This scrim is only shown when we have been given a top system inset.

* * @param color the color to display * @see #getStatusBarScrim() */ public void setStatusBarScrimColor(@ColorInt int color) { setStatusBarScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @see #getStatusBarScrim() */ public void setStatusBarScrimResource(@DrawableRes int resId) { setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the status bar scrim. * * @see #setStatusBarScrim(Drawable) */ @Nullable public Drawable getStatusBarScrim() { return mStatusBarScrim; } /** * Sets the text color and size for the collapsed title from the specified * TextAppearance resource. */ public void setCollapsedTitleTextAppearance(@StyleRes int resId) { mCollapsingTextHelper.setCollapsedTextAppearance(resId); } /** * Sets the text color of the collapsed title. * * @param color The new text color in ARGB format */ public void setCollapsedTitleTextColor(@ColorInt int color) { setCollapsedTitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the collapsed title. * * @param colors ColorStateList containing the new text colors */ public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) { mCollapsedTextColorSkinAttr = 0; mCollapsingTextHelper.setCollapsedTextColor(colors); } public void setCollapsedTextColorSkinAttr(int attr) { mCollapsedTextColorSkinAttr = attr; if (attr != 0) { mCollapsingTextHelper.setCollapsedTextColor( QMUISkinHelper.getSkinColorStateList(this, attr)); } } /** * Sets the horizontal alignment of the collapsed title and the vertical gravity that will * be used when there is extra space in the collapsed bounds beyond what is required for * the title itself. */ public void setCollapsedTitleGravity(int gravity) { mCollapsingTextHelper.setCollapsedTextGravity(gravity); } /** * Returns the horizontal and vertical alignment for title when collapsed. */ public int getCollapsedTitleGravity() { return mCollapsingTextHelper.getCollapsedTextGravity(); } /** * Sets the text color and size for the expanded title from the specified * TextAppearance resource. */ public void setExpandedTitleTextAppearance(@StyleRes int resId) { mCollapsingTextHelper.setExpandedTextAppearance(resId); } /** * Sets the text color of the expanded title. * * @param color The new text color in ARGB format */ public void setExpandedTitleColor(@ColorInt int color) { setExpandedTitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the expanded title. * * @param colors ColorStateList containing the new text colors */ public void setExpandedTitleTextColor(@NonNull ColorStateList colors) { mExpandedTextColorSkinAttr = 0; mCollapsingTextHelper.setExpandedTextColor(colors); } public void setExpandedTextColorSkinAttr(int attr) { mExpandedTextColorSkinAttr = attr; if (attr != 0) { mCollapsingTextHelper.setExpandedTextColor( QMUISkinHelper.getSkinColorStateList(this, attr)); } } /** * Sets the horizontal alignment of the expanded title and the vertical gravity that will * be used when there is extra space in the expanded bounds beyond what is required for * the title itself. */ public void setExpandedTitleGravity(int gravity) { mCollapsingTextHelper.setExpandedTextGravity(gravity); } /** * Returns the horizontal and vertical alignment for title when expanded. */ public int getExpandedTitleGravity() { return mCollapsingTextHelper.getExpandedTextGravity(); } /** * Set the typeface to use for the collapsed title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setCollapsedTitleTypeface(@Nullable Typeface typeface) { mCollapsingTextHelper.setCollapsedTypeface(typeface); } /** * Returns the typeface used for the collapsed title. */ @NonNull public Typeface getCollapsedTitleTypeface() { return mCollapsingTextHelper.getCollapsedTypeface(); } /** * Set the typeface to use for the expanded title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setExpandedTitleTypeface(@Nullable Typeface typeface) { mCollapsingTextHelper.setExpandedTypeface(typeface); } /** * Returns the typeface used for the expanded title. */ @NonNull public Typeface getExpandedTitleTypeface() { return mCollapsingTextHelper.getExpandedTypeface(); } /** * Sets the expanded title margins. * * @param start the starting title margin in pixels * @param top the top title margin in pixels * @param end the ending title margin in pixels * @param bottom the bottom title margin in pixels * @see #getExpandedTitleMarginStart() * @see #getExpandedTitleMarginTop() * @see #getExpandedTitleMarginEnd() * @see #getExpandedTitleMarginBottom() */ public void setExpandedTitleMargin(int start, int top, int end, int bottom) { mExpandedMarginStart = start; mExpandedMarginTop = top; mExpandedMarginEnd = end; mExpandedMarginBottom = bottom; requestLayout(); } /** * @return the starting expanded title margin in pixels * @see #setExpandedTitleMarginStart(int) */ public int getExpandedTitleMarginStart() { return mExpandedMarginStart; } /** * Sets the starting expanded title margin in pixels. * * @param margin the starting title margin in pixels * @see #getExpandedTitleMarginStart() */ public void setExpandedTitleMarginStart(int margin) { mExpandedMarginStart = margin; requestLayout(); } /** * @return the top expanded title margin in pixels * @see #setExpandedTitleMarginTop(int) */ public int getExpandedTitleMarginTop() { return mExpandedMarginTop; } /** * Sets the top expanded title margin in pixels. * * @param margin the top title margin in pixels * @see #getExpandedTitleMarginTop() */ public void setExpandedTitleMarginTop(int margin) { mExpandedMarginTop = margin; requestLayout(); } /** * @return the ending expanded title margin in pixels * @see #setExpandedTitleMarginEnd(int) */ public int getExpandedTitleMarginEnd() { return mExpandedMarginEnd; } /** * Sets the ending expanded title margin in pixels. * * @param margin the ending title margin in pixels * @see #getExpandedTitleMarginEnd() */ public void setExpandedTitleMarginEnd(int margin) { mExpandedMarginEnd = margin; requestLayout(); } /** * @return the bottom expanded title margin in pixels * @see #setExpandedTitleMarginBottom(int) */ public int getExpandedTitleMarginBottom() { return mExpandedMarginBottom; } /** * Sets the bottom expanded title margin in pixels. * * @param margin the bottom title margin in pixels * @see #getExpandedTitleMarginBottom() */ public void setExpandedTitleMarginBottom(int margin) { mExpandedMarginBottom = margin; requestLayout(); } /** * Set the amount of visible height in pixels used to define when to trigger a scrim * visibility change. *

*

If the visible height of this view is less than the given value, the scrims will be * made visible, otherwise they are hidden.

* * @param height value in pixels used to define when to trigger a scrim visibility change */ public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) { if (mScrimVisibleHeightTrigger != height) { mScrimVisibleHeightTrigger = height; // Update the scrim visibility updateScrimVisibility(); } } /** * Returns the amount of visible height in pixels used to define when to trigger a scrim * visibility change. * * @see #setScrimVisibleHeightTrigger(int) */ public int getScrimVisibleHeightTrigger() { if (mScrimVisibleHeightTrigger >= 0) { // If we have one explicitly set, return it return mScrimVisibleHeightTrigger; } // Otherwise we'll use the default computed value final int insetTop = getWindowInsetTop(); final int minHeight = ViewCompat.getMinimumHeight(this); if (minHeight > 0) { // If we have a minHeight set, lets use 2 * minHeight (capped at our height) return Math.min((minHeight * 2) + insetTop, getHeight()); } // If we reach here then we don't have a min height set. Instead we'll take a // guess at 1/3 of our height being visible return getHeight() / 3; } /** * Set the duration used for scrim visibility animations. * * @param duration the duration to use in milliseconds */ public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) { mScrimAnimationDuration = duration; } /** * Returns the duration in milliseconds used for scrim visibility animations. */ public long getScrimAnimationDuration() { return mScrimAnimationDuration; } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof QMUICollapsingTopBarLayout.LayoutParams; } @Override protected QMUICollapsingTopBarLayout.LayoutParams generateDefaultLayoutParams() { return new QMUICollapsingTopBarLayout.LayoutParams(QMUICollapsingTopBarLayout.LayoutParams.MATCH_PARENT, QMUICollapsingTopBarLayout.LayoutParams.MATCH_PARENT); } @Override public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new QMUICollapsingTopBarLayout.LayoutParams(getContext(), attrs); } @Override protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new QMUICollapsingTopBarLayout.LayoutParams(p); } public static class LayoutParams extends FrameLayout.LayoutParams { private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f; @RestrictTo(LIBRARY_GROUP) @IntDef({ COLLAPSE_MODE_OFF, COLLAPSE_MODE_PIN, COLLAPSE_MODE_PARALLAX }) @Retention(RetentionPolicy.SOURCE) public @interface CollapseMode { } /** * The view will act as normal with no collapsing behavior. */ public static final int COLLAPSE_MODE_OFF = 0; /** * The view will pin in place until it reaches the bottom of the * {@link QMUICollapsingTopBarLayout}. */ public static final int COLLAPSE_MODE_PIN = 1; /** * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)} * to change the multiplier used. */ public static final int COLLAPSE_MODE_PARALLAX = 2; int mCollapseMode = COLLAPSE_MODE_OFF; float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.QMUICollapsingTopBarLayout_Layout); mCollapseMode = a.getInt( R.styleable.QMUICollapsingTopBarLayout_Layout_qmui_layout_collapseMode, COLLAPSE_MODE_OFF); setParallaxMultiplier(a.getFloat( R.styleable.QMUICollapsingTopBarLayout_Layout_qmui_layout_collapseParallaxMultiplier, DEFAULT_PARALLAX_MULTIPLIER)); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int gravity) { super(width, height, gravity); } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } @RequiresApi(19) @TargetApi(19) public LayoutParams(FrameLayout.LayoutParams source) { // The copy constructor called here only exists on API 19+. super(source); } /** * Set the collapse mode. * * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} * or {@link #COLLAPSE_MODE_PARALLAX}. */ public void setCollapseMode(@CollapseMode int collapseMode) { mCollapseMode = collapseMode; } /** * Returns the requested collapse mode. * * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} * or {@link #COLLAPSE_MODE_PARALLAX}. */ @CollapseMode public int getCollapseMode() { return mCollapseMode; } /** * Set the parallax scroll multiplier used in conjunction with * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all, * {@code 1.0f} indicates normal scroll movement. * * @param multiplier the multiplier. * @see #getParallaxMultiplier() */ public void setParallaxMultiplier(float multiplier) { mParallaxMult = multiplier; } /** * Returns the parallax scroll multiplier used in conjunction with * {@link #COLLAPSE_MODE_PARALLAX}. * * @see #setParallaxMultiplier(float) */ public float getParallaxMultiplier() { return mParallaxMult; } } /** * Show or hide the scrims if needed */ final void updateScrimVisibility() { if (mContentScrim != null || mStatusBarScrim != null) { setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger()); } } final int getMaxOffsetForPinChild(View child) { final QMUIViewOffsetHelper offsetHelper = getViewOffsetHelper(child); final QMUICollapsingTopBarLayout.LayoutParams lp = (QMUICollapsingTopBarLayout.LayoutParams) child.getLayoutParams(); return getHeight() - offsetHelper.getLayoutTop() - child.getHeight() - lp.bottomMargin; } public void addOnOffsetUpdateListener(@NonNull OnOffsetUpdateListener listener) { mOnOffsetUpdateListeners.add(listener); } public void removeOnOffsetUpdateListener(@NonNull OnOffsetUpdateListener listener) { mOnOffsetUpdateListeners.remove(listener); } private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { OffsetUpdateListener() { } @Override public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { mCurrentOffset = verticalOffset; final int insetTop = getWindowInsetTop(); for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final QMUIViewOffsetHelper offsetHelper = getViewOffsetHelper(child); switch (lp.mCollapseMode) { case QMUICollapsingTopBarLayout.LayoutParams.COLLAPSE_MODE_PIN: offsetHelper.setTopAndBottomOffset( QMUILangHelper.constrain(-verticalOffset, 0, getMaxOffsetForPinChild(child))); break; case QMUICollapsingTopBarLayout.LayoutParams.COLLAPSE_MODE_PARALLAX: offsetHelper.setTopAndBottomOffset( Math.round(-verticalOffset * lp.mParallaxMult)); break; } } // Show or hide the scrims if needed updateScrimVisibility(); if (mStatusBarScrim != null && insetTop > 0) { ViewCompat.postInvalidateOnAnimation(QMUICollapsingTopBarLayout.this); } // Update the collapsing text's fraction final int expandRange = getHeight() - ViewCompat.getMinimumHeight( QMUICollapsingTopBarLayout.this) - insetTop; float expansionFraction = Math.abs(verticalOffset) / (float) expandRange; mCollapsingTextHelper.setExpansionFraction(expansionFraction); for (OnOffsetUpdateListener listener : mOnOffsetUpdateListeners) { listener.onOffsetChanged( QMUICollapsingTopBarLayout.this, verticalOffset, expansionFraction); } } } @Override public boolean intercept(int skinIndex, @NotNull Resources.Theme theme) { if (mContentScrimSkinAttr != 0) { setContentScrimInner(QMUIResHelper.getAttrDrawable(getContext(), theme, mContentScrimSkinAttr)); } if (mStatusBarScrimSkinAttr != 0) { setStatusBarScrimInner(QMUIResHelper.getAttrDrawable(getContext(), theme, mStatusBarScrimSkinAttr)); } if (mCollapsedTextColorSkinAttr != 0) { mCollapsingTextHelper.setCollapsedTextColor( QMUISkinHelper.getSkinColorStateList(this, mCollapsedTextColorSkinAttr)); } if (mExpandedTextColorSkinAttr != 0) { mCollapsingTextHelper.setExpandedTextColor( QMUISkinHelper.getSkinColorStateList(this, mExpandedTextColorSkinAttr) ); } return false; } public interface OnOffsetUpdateListener { void onOffsetChanged(QMUICollapsingTopBarLayout layout, int offset, float expandFraction); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIEmptyView.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.Button; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.annotation.QMUISkinChangeNotAdapted; import androidx.constraintlayout.widget.ConstraintLayout; /** * 用于显示界面的 loading、错误信息提示等状态。 *

* 提供了一个 LoadingView、一行标题、一行说明文字、一个按钮, 可以使用 {@link #show(boolean, String, String, String, OnClickListener)} 系列方法控制这些控件的显示内容 *

*/ public class QMUIEmptyView extends ConstraintLayout { private QMUILoadingView mLoadingView; private TextView mTitleTextView; private TextView mDetailTextView; protected Button mButton; public QMUIEmptyView(Context context) { this(context, null); } public QMUIEmptyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIEmptyView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.QMUIEmptyView); boolean attrShowLoading = arr.getBoolean(R.styleable.QMUIEmptyView_qmui_show_loading, false); String attrTitleText = arr.getString(R.styleable.QMUIEmptyView_qmui_title_text); String attrDetailText = arr.getString(R.styleable.QMUIEmptyView_qmui_detail_text); String attrBtnText = arr.getString(R.styleable.QMUIEmptyView_qmui_btn_text); arr.recycle(); show(attrShowLoading, attrTitleText, attrDetailText, attrBtnText, null); } private void init() { LayoutInflater.from(getContext()).inflate(R.layout.qmui_empty_view, this, true); mLoadingView = findViewById(R.id.empty_view_loading); mTitleTextView = findViewById(R.id.empty_view_title); mDetailTextView = findViewById(R.id.empty_view_detail); mButton = findViewById(R.id.empty_view_button); } /** * 显示emptyView * * @param loading 是否要显示loading * @param titleText 标题的文字,不需要则传null * @param detailText 详情文字,不需要则传null * @param buttonText 按钮的文字,不需要按钮则传null * @param onButtonClickListener 按钮的onClick监听,不需要则传null */ public void show(boolean loading, String titleText, String detailText, String buttonText, OnClickListener onButtonClickListener) { setLoadingShowing(loading); setTitleText(titleText); setDetailText(detailText); setButton(buttonText, onButtonClickListener); show(); } /** * 用于显示emptyView并且只显示loading的情况,此时title、detail、button都被隐藏 * * @param loading 是否显示loading */ public void show(boolean loading) { setLoadingShowing(loading); setTitleText(null); setDetailText(null); setButton(null, null); show(); } /** * 用于显示纯文本的简单调用方法,此时loading、button均被隐藏 * * @param titleText 标题的文字,不需要则传null * @param detailText 详情文字,不需要则传null */ public void show(String titleText, String detailText) { setLoadingShowing(false); setTitleText(titleText); setDetailText(detailText); setButton(null, null); show(); } /** * 显示emptyView,不建议直接使用,建议调用带参数的show()方法,方便控制所有子View的显示/隐藏 */ public void show() { setVisibility(VISIBLE); } /** * 隐藏emptyView */ public void hide() { setVisibility(GONE); setLoadingShowing(false); setTitleText(null); setDetailText(null); setButton(null, null); } public boolean isShowing() { return getVisibility() == VISIBLE; } public boolean isLoading() { return mLoadingView.getVisibility() == VISIBLE; } public void setLoadingShowing(boolean show) { mLoadingView.setVisibility(show ? VISIBLE : GONE); } public void setTitleText(String text) { mTitleTextView.setText(text); mTitleTextView.setVisibility(text != null ? VISIBLE : GONE); } public void setDetailText(String text) { mDetailTextView.setText(text); mDetailTextView.setVisibility(text != null ? VISIBLE : GONE); } @QMUISkinChangeNotAdapted public void setTitleColor(int color) { mTitleTextView.setTextColor(color); } @QMUISkinChangeNotAdapted public void setDetailColor(int color) { mDetailTextView.setTextColor(color); } public void setTitleSkinValue(QMUISkinValueBuilder builder) { QMUISkinHelper.setSkinValue(mTitleTextView, builder); } public void setDetailSkinValue(QMUISkinValueBuilder builder) { QMUISkinHelper.setSkinValue(mDetailTextView, builder); } public void setLoadingSkinValue(QMUISkinValueBuilder builder) { QMUISkinHelper.setSkinValue(mLoadingView, builder); } public void setBtnSkinValue(QMUISkinValueBuilder builder) { QMUISkinHelper.setSkinValue(mButton, builder); } public void setButton(String text, OnClickListener onClickListener) { mButton.setText(text); mButton.setVisibility(text != null ? VISIBLE : GONE); mButton.setOnClickListener(onClickListener); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIFloatLayout.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.widget; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.R; /** * 该 layout 使子 View 类似 CSS 中的 float:left 效果, 从左到右排列子 View 并自动换行。支持以下特性: *
    *
  • 使用 {@link #setChildVerticalSpacing(int)} 和 {@link #setChildHorizontalSpacing(int)} 控制子 View 的垂直/水平间距
  • *
  • 使用 {@link #setGravity(int)} 控制子 View 的对齐方向 (左对齐/居中/右对齐)
  • *
  • 使用 {@link #setMaxNumber(int)} 和 {@link #setMaxLines(int)} 控制子 View 的最多个数或最大行数
  • *
*

在 xml 中采用 {@link com.qmuiteam.qmui.R.styleable#QMUIFloatLayout} 控制以上属性。

*/ public class QMUIFloatLayout extends ViewGroup { private int mChildHorizontalSpacing; private int mChildVerticalSpacing; /** * 对齐方式,目前支持 {@link Gravity#CENTER_HORIZONTAL}, {@link Gravity#LEFT} 和 {@link Gravity#RIGHT} */ private int mGravity; private static final int LINES = 0; private static final int NUMBER = 1; private int mMaxMode = LINES; private int mMaximum = Integer.MAX_VALUE; private int mLineCount = 0; private OnLineCountChangeListener mOnLineCountChangeListener; /** *

每一行的item数目,下标表示行下标,在onMeasured的时候计算得出,供onLayout去使用。

*

若mItemNumberInEachLine[x]==0,则表示第x行已经没有item了

*/ private int[] mItemNumberInEachLine; /** *

每一行的item的宽度和(包括item直接的间距),下标表示行下标, * 如 mWidthSumInEachLine[x]表示第x行的item的宽度和(包括item直接的间距)

*

在onMeasured的时候计算得出,供onLayout去使用

*/ private int[] mWidthSumInEachLine; /** * onMeasure过程中实际参与measure的子View个数 */ private int measuredChildCount; public QMUIFloatLayout(Context context) { this(context, null); } public QMUIFloatLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIFloatLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIFloatLayout); mChildHorizontalSpacing = array.getDimensionPixelSize( R.styleable.QMUIFloatLayout_qmui_childHorizontalSpacing, 0); mChildVerticalSpacing = array.getDimensionPixelSize( R.styleable.QMUIFloatLayout_qmui_childVerticalSpacing, 0); mGravity = array.getInteger(R.styleable.QMUIFloatLayout_android_gravity, Gravity.LEFT); int maxLines = array.getInt(R.styleable.QMUIFloatLayout_android_maxLines, -1); if (maxLines >= 0) { setMaxLines(maxLines); } int maxNumber = array.getInt(R.styleable.QMUIFloatLayout_qmui_maxNumber, -1); if (maxNumber >= 0) { setMaxNumber(maxNumber); } array.recycle(); } @SuppressLint("DrawAllocation") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int maxLineHeight = 0; int resultWidth; int resultHeight; final int count = getChildCount(); mItemNumberInEachLine = new int[count]; mWidthSumInEachLine = new int[count]; int lineIndex = 0; // 若FloatLayout指定了MATCH_PARENT或固定宽度,则需要使子View换行 if (widthSpecMode == MeasureSpec.EXACTLY) { resultWidth = widthSpecSize; measuredChildCount = 0; // 下一个子View的position int childPositionX = getPaddingLeft(); int childPositionY = getPaddingTop(); // 子View的Right最大可达到的x坐标 int childMaxRight = widthSpecSize - getPaddingRight(); for (int i = 0; i < count; i++) { if (mMaxMode == NUMBER && measuredChildCount >= mMaximum) { // 超出最多数量,则不再继续 break; } else if (mMaxMode == LINES && lineIndex >= mMaximum) { // 超出最多行数,则不再继续 break; } final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams childLayoutParams = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), childLayoutParams.width); final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLayoutParams.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); final int childw = child.getMeasuredWidth(); maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight()); // 需要换行 if (childPositionX + childw > childMaxRight) { // 如果换行后超出最大行数,则不再继续 if (mMaxMode == LINES) { if (lineIndex + 1 >= mMaximum) { break; } } mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing; // 后面每次加item都会加上一个space,这样的话每行都会为最后一个item多加一次space,所以在这里减一次 lineIndex++; // 换行 childPositionX = getPaddingLeft(); // 下一行第一个item的x childPositionY += maxLineHeight + mChildVerticalSpacing; // 下一行第一个item的y } mItemNumberInEachLine[lineIndex]++; mWidthSumInEachLine[lineIndex] += (childw + mChildHorizontalSpacing); childPositionX += (childw + mChildHorizontalSpacing); measuredChildCount++; } // 如果最后一个item不是刚好在行末(即lineCount最后没有+1,也就是mWidthSumInEachLine[lineCount]非0),则要减去最后一个item的space if (mWidthSumInEachLine.length > 0 && mWidthSumInEachLine[lineIndex] > 0) { mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing; } if (heightSpecMode == MeasureSpec.UNSPECIFIED) { resultHeight = childPositionY + maxLineHeight + getPaddingBottom(); } else if (heightSpecMode == MeasureSpec.AT_MOST) { resultHeight = childPositionY + maxLineHeight + getPaddingBottom(); resultHeight = Math.min(resultHeight, heightSpecSize); } else { resultHeight = heightSpecSize; } } else { // 不计算换行,直接一行铺开 resultWidth = getPaddingLeft() + getPaddingRight(); measuredChildCount = 0; for (int i = 0; i < count; i++) { if (mMaxMode == NUMBER) { // 超出最多数量,则不再继续 if (measuredChildCount > mMaximum) { break; } } else if (mMaxMode == LINES) { // 超出最大行数,则不再继续 if (1 > mMaximum) { break; } } final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams childLayoutParams = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), childLayoutParams.width); final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLayoutParams.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); resultWidth += child.getMeasuredWidth(); maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight()); measuredChildCount++; } if (measuredChildCount > 0) { resultWidth += mChildHorizontalSpacing * (measuredChildCount - 1); } resultHeight = maxLineHeight + getPaddingTop() + getPaddingBottom(); if (mItemNumberInEachLine.length > 0) { mItemNumberInEachLine[lineIndex] = count; } if (mWidthSumInEachLine.length > 0) { mWidthSumInEachLine[0] = resultWidth; } } setMeasuredDimension(resultWidth, resultHeight); int meausureLineCount = lineIndex + 1; if(mLineCount != meausureLineCount){ if(mOnLineCountChangeListener != null){ mOnLineCountChangeListener.onChange(mLineCount, meausureLineCount); } mLineCount = meausureLineCount; } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = right - left; // 按照不同gravity使用不同的布局,默认是left switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: layoutWithGravityLeft(width); break; case Gravity.RIGHT: layoutWithGravityRight(width); break; case Gravity.CENTER_HORIZONTAL: layoutWithGravityCenterHorizontal(width); break; default: layoutWithGravityLeft(width); break; } } /** * 将子View居中布局 */ private void layoutWithGravityCenterHorizontal(int parentWidth) { int nextChildIndex = 0; int nextChildPositionX; int nextChildPositionY = getPaddingTop(); int lineHeight = 0; int layoutChildCount = 0; int layoutChildEachLine = 0; // 遍历每一行 for (int i = 0; i < mItemNumberInEachLine.length; i++) { // 如果这一行已经没item了,则退出循环 if (mItemNumberInEachLine[i] == 0) { break; } // 遍历该行内的元素,布局每个元素 nextChildPositionX = (parentWidth - getPaddingLeft() - getPaddingRight() - mWidthSumInEachLine[i]) / 2 + getPaddingLeft(); // 子 View 的最小 x 值 while (layoutChildEachLine < mItemNumberInEachLine[i]) { final View childView = getChildAt(nextChildIndex); if (childView.getVisibility() == GONE) { nextChildIndex++; continue; } final int childw = childView.getMeasuredWidth(); final int childh = childView.getMeasuredHeight(); childView.layout(nextChildPositionX, nextChildPositionY, nextChildPositionX + childw, nextChildPositionY + childh); lineHeight = Math.max(lineHeight, childh); nextChildPositionX += childw + mChildHorizontalSpacing; layoutChildCount++; layoutChildEachLine++; nextChildIndex++; if (layoutChildCount == measuredChildCount) { break; } } if (layoutChildCount == measuredChildCount) { break; } // 一行结束了,整理一下,准备下一行 nextChildPositionY += (lineHeight + mChildVerticalSpacing); lineHeight = 0; layoutChildEachLine = 0; } int childCount = getChildCount(); for (int i = nextChildIndex; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() == View.GONE) { continue; } childView.layout(0, 0, 0, 0); } } /** * 将子View靠左布局 */ private void layoutWithGravityLeft(int parentWidth) { int childMaxRight = parentWidth - getPaddingRight(); int childPositionX = getPaddingLeft(); int childPositionY = getPaddingTop(); int lineHeight = 0; final int childCount = getChildCount(); int layoutChildCount = 0; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } if (layoutChildCount < measuredChildCount) { final int childw = child.getMeasuredWidth(); final int childh = child.getMeasuredHeight(); if (childPositionX + childw > childMaxRight) { // 换行 childPositionX = getPaddingLeft(); childPositionY += (lineHeight + mChildVerticalSpacing); lineHeight = 0; } child.layout(childPositionX, childPositionY, childPositionX + childw, childPositionY + childh); childPositionX += childw + mChildHorizontalSpacing; lineHeight = Math.max(lineHeight, childh); layoutChildCount++; } else { child.layout(0, 0, 0, 0); } } } /** * 将子View靠右布局 */ private void layoutWithGravityRight(int parentWidth) { int nextChildIndex = 0; int nextChildPositionX; int nextChildPositionY = getPaddingTop(); int lineHeight = 0; int layoutChildCount = 0; int layoutChildEachLine = 0; // 遍历每一行 for (int i = 0; i < mItemNumberInEachLine.length; i++) { // 如果这一行已经没item了,则退出循环 if (mItemNumberInEachLine[i] == 0) { break; } // 遍历该行内的元素,布局每个元素 nextChildPositionX = parentWidth - getPaddingRight() - mWidthSumInEachLine[i]; // 初始值为子 View 的最小 x 值 while (layoutChildEachLine < mItemNumberInEachLine[i]) { final View childView = getChildAt(nextChildIndex); if (childView.getVisibility() == GONE) { nextChildIndex++; continue; } final int childw = childView.getMeasuredWidth(); final int childh = childView.getMeasuredHeight(); childView.layout(nextChildPositionX, nextChildPositionY, nextChildPositionX + childw, nextChildPositionY + childh); lineHeight = Math.max(lineHeight, childh); nextChildPositionX += childw + mChildHorizontalSpacing; layoutChildCount++; layoutChildEachLine++; nextChildIndex++; if (layoutChildCount == measuredChildCount) { break; } } if (layoutChildCount == measuredChildCount) { break; } // 一行结束了,整理一下,准备下一行 nextChildPositionY += (lineHeight + mChildVerticalSpacing); lineHeight = 0; layoutChildEachLine = 0; } int childCount = getChildCount(); for (int i = nextChildIndex; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() == View.GONE) { continue; } childView.layout(0, 0, 0, 0); } } /** * 设置子 View 的对齐方式,目前支持 {@link Gravity#CENTER_HORIZONTAL}, {@link Gravity#LEFT} 和 {@link Gravity#RIGHT} */ public void setGravity(int gravity) { if (mGravity != gravity) { mGravity = gravity; requestLayout(); } } public int getGravity() { return mGravity; } /** * 设置最多可显示的子View个数 * 注意该方法不会改变子View的个数,只会影响显示出来的子View个数 * * @param maxNumber 最多可显示的子View个数 */ public void setMaxNumber(int maxNumber) { mMaximum = maxNumber; mMaxMode = NUMBER; requestLayout(); } /** * 获取最多可显示的子View个数 */ public int getMaxNumber() { return mMaxMode == NUMBER ? mMaximum : -1; } /** * 设置最多可显示的行数 * 注意该方法不会改变子View的个数,只会影响显示出来的子View个数 * * @param maxLines 最多可显示的行数 */ public void setMaxLines(int maxLines) { mMaximum = maxLines; mMaxMode = LINES; requestLayout(); } public void setOnLineCountChangeListener(OnLineCountChangeListener onLineCountChangeListener) { mOnLineCountChangeListener = onLineCountChangeListener; } public int getLineCount() { return mLineCount; } /** * 获取最多可显示的行数 * * @return 没有限制时返回-1 */ public int getMaxLines() { return mMaxMode == LINES ? mMaximum : -1; } /** * 设置子 View 的水平间距 */ public void setChildHorizontalSpacing(int spacing) { mChildHorizontalSpacing = spacing; invalidate(); } /** * 设置子 View 的垂直间距 */ public void setChildVerticalSpacing(int spacing) { mChildVerticalSpacing = spacing; invalidate(); } public interface OnLineCountChangeListener { void onChange(int oldLineCount, int newLineCount); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIFontFitTextView.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import androidx.appcompat.widget.AppCompatTextView; /** * 使 {@link android.widget.TextView} 在宽度固定的情况下,文字多到一行放不下时能缩小文字大小来自适应 * * http://stackoverflow.com/questions/2617266/how-to-adjust-text-font-size-to-fit-textview */ public class QMUIFontFitTextView extends AppCompatTextView { private Paint mTestPaint; private float minSize; private float maxSize; public QMUIFontFitTextView(Context context) { this(context, null); } public QMUIFontFitTextView(Context context, AttributeSet attrs) { super(context, attrs); mTestPaint = new Paint(); mTestPaint.set(this.getPaint()); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIFontFitTextView); minSize = array.getDimensionPixelSize( R.styleable.QMUIFontFitTextView_qmui_minTextSize, Math.round(14 * QMUIDisplayHelper.DENSITY)); maxSize = array.getDimensionPixelSize( R.styleable.QMUIFontFitTextView_qmui_maxTextSize, Math.round(18 * QMUIDisplayHelper.DENSITY)); array.recycle(); //max size defaults to the initially specified text size unless it is too small } /* Re size the font so the specified text fits in the text box * assuming the text box is the specified width. */ private void refitText(String text, int textWidth) { if (textWidth <= 0) return; int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight(); float hi = maxSize; float lo = minSize; float size; final float threshold = 0.5f; // How close we have to be mTestPaint.set(this.getPaint()); mTestPaint.setTextSize(maxSize); if(mTestPaint.measureText(text) <= targetWidth) { lo = maxSize; } else { mTestPaint.setTextSize(minSize); if(mTestPaint.measureText(text) < targetWidth) { while((hi - lo) > threshold) { size = (hi+lo)/2; mTestPaint.setTextSize(size); if(mTestPaint.measureText(text) >= targetWidth) hi = size; // too big else lo = size; // too small } } } // Use lo so that we undershoot rather than overshoot this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int height = getMeasuredHeight(); refitText(this.getText().toString(), parentWidth); this.setMeasuredDimension(parentWidth, height); } @Override protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { refitText(text.toString(), this.getWidth()); } @Override protected void onSizeChanged (int w, int h, int oldw, int oldh) { if (w != oldw) { refitText(this.getText().toString(), w); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIItemViewsAdapter.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.widget; import android.view.View; import android.view.ViewGroup; import androidx.core.util.Pools; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.R; import java.util.ArrayList; import java.util.List; /** * 一个带 cache 功能的“列表型数据-View”的适配器,适用于自定义 {@link View} 需要显示重复单元 {@link android.widget.ListView} 的情景, * cache 功能主要是保证在需要多次刷新数据或布局的情况下({@link android.widget.ListView} 或 {@link RecyclerView} 的 itemView) * 复用已存在的 {@link View}。 * QMUI 用于 {@link com.qmuiteam.qmui.widget.tab.QMUITabSegment} 中 {@link com.qmuiteam.qmui.widget.tab.QMUITab} 与数据的适配。 * * @author cginechen * @date 2016-11-27 */ public abstract class QMUIItemViewsAdapter { private Pools.Pool mCachePool; private List mItemData = new ArrayList<>(); // 不能简单的用mParentView的子views,因为可能mParentView有一些装饰子view,不应该归adapter管理 private List mViews = new ArrayList<>(); private ViewGroup mParentView; public QMUIItemViewsAdapter(ViewGroup parentView) { mParentView = parentView; } public void detach(int count) { int childCount = mViews.size(); while (childCount > 0 && count > 0) { V view = mViews.remove(childCount - 1); if (mCachePool == null) { mCachePool = new Pools.SimplePool<>(12); } // 做简单cache,如果V需要动态添加子view,则业务保证不做cache Object notCacheTag = view.getTag(R.id.qmui_view_can_not_cache_tag); if (notCacheTag == null || !(boolean) notCacheTag) { try { onViewRecycled(view); mCachePool.release(view); } catch (Exception ignored) { } } mParentView.removeView(view); childCount--; count--; } } public void clear() { mItemData.clear(); detach(mViews.size()); } private V getView() { V v = mCachePool != null ? mCachePool.acquire() : null; if (v == null) { v = createView(mParentView); } return v; } protected abstract V createView(ViewGroup parentView); protected void onViewRecycled(V v){ } public QMUIItemViewsAdapter addItem(T item) { mItemData.add(item); return this; } public void setup() { int itemCount = mItemData.size(); int childCount = mViews.size(); int i; if (childCount > itemCount) { detach(childCount - itemCount); } else if (childCount < itemCount) { for (i = 0; i < itemCount - childCount; i++) { V view = getView(); mParentView.addView(view); mViews.add(view); } } for (i = 0; i < itemCount; i++) { V view = mViews.get(i); T item = mItemData.get(i); bind(item, view, i); } mParentView.invalidate(); mParentView.requestLayout(); } public T getItem(int position) { if (mItemData == null) { return null; } if (position < 0 || position >= mItemData.size()) { return null; } return mItemData.get(position); } public void replaceItem(int position, T data) throws IllegalAccessException { if (position < mItemData.size() && position >= 0) { mItemData.set(position, data); } else { throw new IllegalAccessException("替换数据不存在"); } } protected abstract void bind(T item, V view, int position); public List getViews() { return mViews; } public int getSize() { if (mItemData == null) { return 0; } return mItemData.size(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUILoadingView.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.widget; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import androidx.annotation.NonNull; import androidx.collection.SimpleArrayMap; /** * 用于显示 Loading 的 {@link View},支持颜色和大小的设置。 * * @author cginechen * @date 2016-09-21 */ public class QMUILoadingView extends View implements IQMUISkinDefaultAttrProvider { private int mSize; private int mPaintColor; private int mAnimateValue = 0; private ValueAnimator mAnimator; private Paint mPaint; private static final int LINE_COUNT = 12; private static final int DEGREE_PER_LINE = 360 / LINE_COUNT; private static SimpleArrayMap sDefaultAttrs; static { sDefaultAttrs = new SimpleArrayMap<>(); sDefaultAttrs.put(QMUISkinValueBuilder.TINT_COLOR, R.attr.qmui_skin_support_loading_color); } public QMUILoadingView(Context context) { this(context, null); } public QMUILoadingView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUILoadingStyle); } public QMUILoadingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.QMUILoadingView, defStyleAttr, 0); mSize = array.getDimensionPixelSize(R.styleable.QMUILoadingView_qmui_loading_view_size, QMUIDisplayHelper.dp2px(context, 32)); mPaintColor = array.getInt(R.styleable.QMUILoadingView_android_color, Color.WHITE); array.recycle(); initPaint(); } public QMUILoadingView(Context context, int size, int color) { super(context); mSize = size; mPaintColor = color; initPaint(); } private void initPaint() { mPaint = new Paint(); mPaint.setColor(mPaintColor); mPaint.setAntiAlias(true); mPaint.setStrokeCap(Paint.Cap.ROUND); } public void setColor(int color) { mPaintColor = color; mPaint.setColor(color); invalidate(); } public void setSize(int size) { mSize = size; requestLayout(); } private ValueAnimator.AnimatorUpdateListener mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimateValue = (int) animation.getAnimatedValue(); invalidate(); } }; public void start() { if (mAnimator == null) { mAnimator = ValueAnimator.ofInt(0, LINE_COUNT - 1); mAnimator.addUpdateListener(mUpdateListener); mAnimator.setDuration(600); mAnimator.setRepeatMode(ValueAnimator.RESTART); mAnimator.setRepeatCount(ValueAnimator.INFINITE); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.start(); } else if (!mAnimator.isStarted()) { mAnimator.start(); } } public void stop() { if (mAnimator != null) { mAnimator.removeUpdateListener(mUpdateListener); mAnimator.removeAllUpdateListeners(); mAnimator.cancel(); mAnimator = null; } } private void drawLoading(Canvas canvas, int rotateDegrees) { int width = mSize / 12, height = mSize / 6; mPaint.setStrokeWidth(width); canvas.rotate(rotateDegrees, mSize / 2, mSize / 2); canvas.translate(mSize / 2, mSize / 2); for (int i = 0; i < LINE_COUNT; i++) { canvas.rotate(DEGREE_PER_LINE); mPaint.setAlpha((int) (255f * (i + 1) / LINE_COUNT)); canvas.translate(0, -mSize / 2 + width / 2); canvas.drawLine(0, 0, 0, height, mPaint); canvas.translate(0, mSize / 2 - width / 2); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mSize, mSize); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); drawLoading(canvas, mAnimateValue * DEGREE_PER_LINE); canvas.restoreToCount(saveCount); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); start(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stop(); } @Override protected void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility == VISIBLE) { start(); } else { stop(); } } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultAttrs; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUINotchConsumeLayout.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.widget; import android.content.Context; import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.util.QMUINotchHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; public class QMUINotchConsumeLayout extends FrameLayout { public QMUINotchConsumeLayout(Context context) { this(context, null); } public QMUINotchConsumeLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUINotchConsumeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); QMUIWindowInsetHelper.setOnApplyWindowInsetsListener(this, new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { notifyInsetMaybeChanged(); return insets; } }, true); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!QMUINotchHelper.isNotchOfficialSupport()) { notifyInsetMaybeChanged(); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (!QMUINotchHelper.isNotchOfficialSupport()) { notifyInsetMaybeChanged(); } } public boolean notifyInsetMaybeChanged() { setPadding( QMUINotchHelper.getSafeInsetLeft(this), QMUINotchHelper.getSafeInsetTop(this), QMUINotchHelper.getSafeInsetRight(this), QMUINotchHelper.getSafeInsetBottom(this) ); return true; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIObservableScrollView.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.widget; import android.content.Context; import android.util.AttributeSet; import android.widget.ScrollView; import java.util.ArrayList; import java.util.List; /** * 可以监听滚动事件的 {@link ScrollView},并能在滚动回调中获取每次滚动前后的偏移量。 *

* 由于 {@link ScrollView} 没有类似于 addOnScrollChangedListener 的方法可以监听滚动事件,所以需要通过重写 {@link android.view.View#onScrollChanged},来触发滚动监听 * * @author chantchen * @date 2015-08-25 */ public class QMUIObservableScrollView extends ScrollView { private int mScrollOffset = 0; private List mOnScrollChangedListeners; public QMUIObservableScrollView(Context context) { super(context); } public QMUIObservableScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void addOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) { if (mOnScrollChangedListeners == null) { mOnScrollChangedListeners = new ArrayList<>(); } if (mOnScrollChangedListeners.contains(onScrollChangedListener)) { return; } mOnScrollChangedListeners.add(onScrollChangedListener); } public void removeOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) { if (mOnScrollChangedListeners == null) { return; } mOnScrollChangedListeners.remove(onScrollChangedListener); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); mScrollOffset = t; if (mOnScrollChangedListeners != null && !mOnScrollChangedListeners.isEmpty()) { for (OnScrollChangedListener listener : mOnScrollChangedListeners) { listener.onScrollChanged(this, l, t, oldl, oldt); } } } public int getScrollOffset() { return mScrollOffset; } public interface OnScrollChangedListener { void onScrollChanged(QMUIObservableScrollView scrollView, int l, int t, int oldl, int oldt); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIPagerAdapter.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.widget; import android.util.SparseArray; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; /** * @author cginechen * @date 2017-09-13 */ public abstract class QMUIPagerAdapter extends PagerAdapter { private SparseArray mScrapItems = new SparseArray<>(); public QMUIPagerAdapter() { } /** * Hydrating an object is taking an object that exists in memory, * that doesn't yet contain any domain data ("real" data), * and then populating it with domain data. */ @NonNull protected abstract Object hydrate(@NonNull ViewGroup container, int position); protected abstract void populate(@NonNull ViewGroup container, @NonNull Object item, int position); protected abstract void destroy(@NonNull ViewGroup container, int position, @NonNull Object object); @Override @NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { Object item = mScrapItems.get(position); if (item == null) { item = hydrate(container, position); mScrapItems.put(position, item); } populate(container, item, position); return item; } @Override public final void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { destroy(container, position, object); } /** * sometimes you may need to perform some operations on all items, * such as perform cleanup when the ViewPager is destroyed * once the action return true, then do not handle remain items * * @param action */ public void each(@NonNull Action action) { int size = mScrapItems.size(); for (int i = 0; i < size; i++) { Object item = mScrapItems.valueAt(i); if (action.call(item)) { break; } } } public interface Action { /** * @return true to intercept forEach */ boolean call(Object item); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIProgressBar.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; /** * 一个进度条控件,通过颜色变化显示进度,支持环形和矩形两种形式,主要特性如下: *
    *
  1. 支持在进度条中以文字形式显示进度,支持修改文字的颜色和大小。
  2. *
  3. 可以通过 xml 属性修改进度背景色,当前进度颜色,进度条尺寸。
  4. *
  5. 支持限制进度的最大值。
  6. *
* * @author cginechen * @date 2015-07-29 */ public class QMUIProgressBar extends View { public final static int TYPE_RECT = 0; public final static int TYPE_ROUND_RECT = 1; public final static int TYPE_CIRCLE = 2; public final static int TYPE_FILL_CIRCLE = 3; public final static int TOTAL_DURATION = 1000; public final static int DEFAULT_PROGRESS_COLOR = Color.BLUE; public final static int DEFAULT_BACKGROUND_COLOR = Color.GRAY; public final static int DEFAULT_TEXT_SIZE = 20; public final static int DEFAULT_TEXT_COLOR = Color.BLACK; private final static int PENDING_VALUE_NOT_SET = -1; /*circle_progress member*/ public static int DEFAULT_STROKE_WIDTH = QMUIDisplayHelper.dpToPx(40); QMUIProgressBarTextGenerator mQMUIProgressBarTextGenerator; /*rect_progress member*/ RectF mBgRect; RectF mProgressRect; /*common member*/ private int mWidth; private int mHeight; private int mType; private int mProgressColor; private int mBackgroundColor; private int mMaxValue; private int mValue; private int mPendingValue; private long mAnimationStartTime; private int mAnimationDistance; private int mAnimationDuration; private int mTextSize; private int mTextColor; private boolean mRoundCap; private Paint mBackgroundPaint = new Paint(); private Paint mPaint = new Paint(); private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private RectF mArcOval = new RectF(); private String mText = ""; private int mStrokeWidth; private float mCircleRadius; private Point mCenterPoint; private OnProgressChangeListener mOnProgressChangeListener; private Runnable mNotifyProgressChangeAction = new Runnable() { @Override public void run() { if(mOnProgressChangeListener != null){ mOnProgressChangeListener.onProgressChange(QMUIProgressBar.this, mValue, mMaxValue); } } }; public QMUIProgressBar(Context context) { super(context); setup(context, null); } public QMUIProgressBar(Context context, AttributeSet attrs) { super(context, attrs); setup(context, attrs); } public QMUIProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setup(context, attrs); } public void setup(Context context, AttributeSet attrs) { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIProgressBar); mType = array.getInt(R.styleable.QMUIProgressBar_qmui_type, TYPE_RECT); mProgressColor = array.getColor(R.styleable.QMUIProgressBar_qmui_progress_color, DEFAULT_PROGRESS_COLOR); mBackgroundColor = array.getColor(R.styleable.QMUIProgressBar_qmui_background_color, DEFAULT_BACKGROUND_COLOR); mMaxValue = array.getInt(R.styleable.QMUIProgressBar_qmui_max_value, 100); mValue = array.getInt(R.styleable.QMUIProgressBar_qmui_value, 0); mRoundCap = array.getBoolean(R.styleable.QMUIProgressBar_qmui_stroke_round_cap, false); mTextSize = DEFAULT_TEXT_SIZE; if (array.hasValue(R.styleable.QMUIProgressBar_android_textSize)) { mTextSize = array.getDimensionPixelSize(R.styleable.QMUIProgressBar_android_textSize, DEFAULT_TEXT_SIZE); } mTextColor = DEFAULT_TEXT_COLOR; if (array.hasValue(R.styleable.QMUIProgressBar_android_textColor)) { mTextColor = array.getColor(R.styleable.QMUIProgressBar_android_textColor, DEFAULT_TEXT_COLOR); } if (mType == TYPE_CIRCLE || mType == TYPE_FILL_CIRCLE) { mStrokeWidth = array.getDimensionPixelSize(R.styleable.QMUIProgressBar_qmui_stroke_width, DEFAULT_STROKE_WIDTH); } array.recycle(); configPaint(mTextColor, mTextSize, mRoundCap, mStrokeWidth); setProgress(mValue); } public void setOnProgressChangeListener(OnProgressChangeListener onProgressChangeListener) { mOnProgressChangeListener = onProgressChangeListener; } public void setStrokeWidth(int strokeWidth) { if(mStrokeWidth != strokeWidth){ mStrokeWidth = strokeWidth; if(mWidth > 0){ configShape(); } configPaint(mTextColor, mTextSize, mRoundCap, mStrokeWidth); invalidate(); } } private void configShape() { if (mType == TYPE_RECT || mType == TYPE_ROUND_RECT) { mBgRect = new RectF(getPaddingLeft(), getPaddingTop(), mWidth + getPaddingLeft(), mHeight + getPaddingTop()); mProgressRect = new RectF(); } else { mCircleRadius = (Math.min(mWidth, mHeight) - mStrokeWidth) / 2f - 0.5f; mCenterPoint = new Point(mWidth / 2, mHeight / 2); } } private void configPaint(int textColor, int textSize, boolean isRoundCap, int strokeWidth) { mPaint.setColor(mProgressColor); mBackgroundPaint.setColor(mBackgroundColor); if (mType == TYPE_RECT || mType == TYPE_ROUND_RECT) { mPaint.setStyle(Paint.Style.FILL); mPaint.setStrokeCap(Paint.Cap.BUTT); mBackgroundPaint.setStyle(Paint.Style.FILL); } else if(mType == TYPE_FILL_CIRCLE){ mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); mPaint.setStrokeCap(Paint.Cap.BUTT); mBackgroundPaint.setStyle(Paint.Style.STROKE); mBackgroundPaint.setStrokeWidth(strokeWidth); mBackgroundPaint.setAntiAlias(true); } else { mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(strokeWidth); mPaint.setAntiAlias(true); if (isRoundCap) { mPaint.setStrokeCap(Paint.Cap.ROUND); }else{ mPaint.setStrokeCap(Paint.Cap.BUTT); } mBackgroundPaint.setStyle(Paint.Style.STROKE); mBackgroundPaint.setStrokeWidth(strokeWidth); mBackgroundPaint.setAntiAlias(true); } mTextPaint.setColor(textColor); mTextPaint.setTextSize(textSize); mTextPaint.setTextAlign(Paint.Align.CENTER); } public void setType(int type) { mType = type; configPaint(mTextColor, mTextSize, mRoundCap, mStrokeWidth); invalidate(); } public void setBarColor(int backgroundColor, int progressColor) { mBackgroundColor = backgroundColor; mProgressColor = progressColor; mBackgroundPaint.setColor(mBackgroundColor); mPaint.setColor(mProgressColor); invalidate(); } @Override public void setBackgroundColor(int backgroundColor) { mBackgroundColor = backgroundColor; mBackgroundPaint.setColor(mBackgroundColor); invalidate(); } public void setProgressColor(int progressColor) { mProgressColor = progressColor; mPaint.setColor(mProgressColor); invalidate(); } /** * 设置进度文案的文字大小 * * @see #setTextColor(int) * @see #setQMUIProgressBarTextGenerator(QMUIProgressBarTextGenerator) */ public void setTextSize(int textSize) { mTextPaint.setTextSize(textSize); invalidate(); } /** * 设置进度文案的文字颜色 * * @see #setTextSize(int) * @see #setQMUIProgressBarTextGenerator(QMUIProgressBarTextGenerator) */ public void setTextColor(int textColor) { mTextPaint.setColor(textColor); invalidate(); } /** * 设置环形进度条的两端是否有圆形的线帽,类型为{@link #TYPE_CIRCLE}时生效 */ public void setStrokeRoundCap(boolean isRoundCap) { mPaint.setStrokeCap(isRoundCap ? Paint.Cap.ROUND : Paint.Cap.BUTT); invalidate(); } /** * 通过 {@link QMUIProgressBarTextGenerator} 设置进度文案 */ public void setQMUIProgressBarTextGenerator(QMUIProgressBarTextGenerator QMUIProgressBarTextGenerator) { mQMUIProgressBarTextGenerator = QMUIProgressBarTextGenerator; } public QMUIProgressBarTextGenerator getQMUIProgressBarTextGenerator() { return mQMUIProgressBarTextGenerator; } @Override protected void onDraw(Canvas canvas) { if (mPendingValue != PENDING_VALUE_NOT_SET) { long elapsed = System.currentTimeMillis() - mAnimationStartTime; if (elapsed >= mAnimationDuration) { mValue = mPendingValue; post(mNotifyProgressChangeAction); mPendingValue = PENDING_VALUE_NOT_SET; } else { mValue = (int) (mPendingValue - (1f - ((float) elapsed / mAnimationDuration)) * mAnimationDistance); post(mNotifyProgressChangeAction); ViewCompat.postInvalidateOnAnimation(this); } } if (mQMUIProgressBarTextGenerator != null) { mText = mQMUIProgressBarTextGenerator.generateText(this, mValue, mMaxValue); } if(((mType == TYPE_RECT || mType == TYPE_ROUND_RECT) && mBgRect == null) || ((mType == TYPE_CIRCLE || mType == TYPE_FILL_CIRCLE) && mCenterPoint == null)){ // npe protect, sometimes measure may not be called by parent. configShape(); } if (mType == TYPE_RECT) { drawRect(canvas); } else if (mType == TYPE_ROUND_RECT) { drawRoundRect(canvas); } else { drawCircle(canvas, mType == TYPE_FILL_CIRCLE); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); mHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); configShape(); setMeasuredDimension(mWidth, mHeight); } private void drawRect(Canvas canvas) { canvas.drawRect(mBgRect, mBackgroundPaint); mProgressRect.set(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + parseValueToWidth(), getPaddingTop() + mHeight); canvas.drawRect(mProgressRect, mPaint); if (mText != null && mText.length() > 0) { Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); float baseline = mBgRect.top + (mBgRect.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; canvas.drawText(mText, mBgRect.centerX(), baseline, mTextPaint); } } private void drawRoundRect(Canvas canvas) { float round = mHeight / 2f; canvas.drawRoundRect(mBgRect, round, round, mBackgroundPaint); mProgressRect.set(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + parseValueToWidth(), getPaddingTop() + mHeight); canvas.drawRoundRect(mProgressRect, round, round, mPaint); if (mText != null && mText.length() > 0) { Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); float baseline = mBgRect.top + (mBgRect.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; canvas.drawText(mText, mBgRect.centerX(), baseline, mTextPaint); } } private void drawCircle(Canvas canvas, boolean useCenter) { canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mCircleRadius, mBackgroundPaint); mArcOval.left = mCenterPoint.x - mCircleRadius; mArcOval.right = mCenterPoint.x + mCircleRadius; mArcOval.top = mCenterPoint.y - mCircleRadius; mArcOval.bottom = mCenterPoint.y + mCircleRadius; if (mValue > 0) { canvas.drawArc(mArcOval, 270, 360f * mValue / mMaxValue, useCenter, mPaint); } if (mText != null && mText.length() > 0) { Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt(); float baseline = mArcOval.top + (mArcOval.height() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; canvas.drawText(mText, mCenterPoint.x, baseline, mTextPaint); } } private int parseValueToWidth() { return mWidth * mValue / mMaxValue; } public int getProgress() { return mValue; } public void setProgress(int progress) { setProgress(progress, true); } public void setProgress(int progress, boolean animated) { if (progress > mMaxValue || progress < 0) { return; } if ((mPendingValue == PENDING_VALUE_NOT_SET && mValue == progress) || (mPendingValue != PENDING_VALUE_NOT_SET && mPendingValue == progress)) { return; } if (!animated) { mPendingValue = PENDING_VALUE_NOT_SET; mValue = progress; mNotifyProgressChangeAction.run(); invalidate(); } else { mAnimationDuration = Math.abs((int) (TOTAL_DURATION * (mValue - progress) / (float) mMaxValue)); mAnimationStartTime = System.currentTimeMillis(); mAnimationDistance = progress - mValue; mPendingValue = progress; invalidate(); } } public int getMaxValue() { return mMaxValue; } public void setMaxValue(int maxValue) { mMaxValue = maxValue; } public interface QMUIProgressBarTextGenerator { /** * 设置进度文案, {@link QMUIProgressBar} 会在进度更新时调用该方法获取要显示的文案 * * @param value 当前进度值 * @param maxValue 最大进度值 * @return 进度文案 */ String generateText(QMUIProgressBar progressBar, int value, int maxValue); } public interface OnProgressChangeListener { void onProgressChange(QMUIProgressBar progressBar, int currentValue, int maxValue); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import com.qmuiteam.qmui.R; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageView; /** * 提供为图片添加圆角、边框、剪裁到圆形或其他形状等功能。 * shown radius image in view, is different to {@link QMUIRadiusImageView2} * * @author cginechen * @date 2015-07-09 */ public class QMUIRadiusImageView extends AppCompatImageView { private static final int DEFAULT_BORDER_COLOR = Color.GRAY; private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; private static final int COLOR_DRAWABLE_DIMEN = 2; private boolean mIsSelected = false; private boolean mIsOval = false; private boolean mIsCircle = false; private int mBorderWidth; private int mBorderColor; private int mSelectedBorderWidth; private int mSelectedBorderColor; private int mSelectedMaskColor; private boolean mIsTouchSelectModeEnabled = true; private int mCornerRadius; private Paint mBitmapPaint; private Paint mBorderPaint; private ColorFilter mColorFilter; private ColorFilter mSelectedColorFilter; private BitmapShader mBitmapShader; private boolean mNeedResetShader = false; private RectF mRectF = new RectF(); private RectF mDrawRectF = new RectF(); private Bitmap mBitmap; private Matrix mMatrix; private int mWidth; private int mHeight; private ScaleType mLastCalculateScaleType; public QMUIRadiusImageView(Context context) { this(context, null, R.attr.QMUIRadiusImageViewStyle); } public QMUIRadiusImageView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUIRadiusImageViewStyle); } public QMUIRadiusImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mBorderPaint = new Paint(); mBorderPaint.setAntiAlias(true); mBorderPaint.setStyle(Paint.Style.STROKE); mMatrix = new Matrix(); setScaleType(ScaleType.CENTER_CROP); TypedArray array = context.obtainStyledAttributes( attrs, R.styleable.QMUIRadiusImageView, defStyleAttr, 0); mBorderWidth = array.getDimensionPixelSize(R.styleable.QMUIRadiusImageView_qmui_border_width, 0); mBorderColor = array.getColor(R.styleable.QMUIRadiusImageView_qmui_border_color, DEFAULT_BORDER_COLOR); mSelectedBorderWidth = array.getDimensionPixelSize( R.styleable.QMUIRadiusImageView_qmui_selected_border_width, mBorderWidth); mSelectedBorderColor = array.getColor( R.styleable.QMUIRadiusImageView_qmui_selected_border_color, mBorderColor); mSelectedMaskColor = array.getColor( R.styleable.QMUIRadiusImageView_qmui_selected_mask_color, Color.TRANSPARENT); if (mSelectedMaskColor != Color.TRANSPARENT) { mSelectedColorFilter = new PorterDuffColorFilter(mSelectedMaskColor, PorterDuff.Mode.DARKEN); } mIsTouchSelectModeEnabled = array.getBoolean( R.styleable.QMUIRadiusImageView_qmui_is_touch_select_mode_enabled, true); mIsCircle = array.getBoolean(R.styleable.QMUIRadiusImageView_qmui_is_circle, false); if (!mIsCircle) { mIsOval = array.getBoolean(R.styleable.QMUIRadiusImageView_qmui_is_oval, false); } if (!mIsOval) { mCornerRadius = array.getDimensionPixelSize( R.styleable.QMUIRadiusImageView_qmui_corner_radius, 0); } array.recycle(); } @Override public void setAdjustViewBounds(boolean adjustViewBounds) { if (adjustViewBounds) { throw new IllegalArgumentException("不支持adjustViewBounds"); } } public void setBorderWidth(int borderWidth) { if (mBorderWidth != borderWidth) { mBorderWidth = borderWidth; invalidate(); } } public void setBorderColor(@ColorInt int borderColor) { if (mBorderColor != borderColor) { mBorderColor = borderColor; invalidate(); } } public void setCornerRadius(int cornerRadius) { if (mCornerRadius != cornerRadius) { mCornerRadius = cornerRadius; if (!mIsCircle && !mIsOval) { invalidate(); } } } public void setSelectedBorderColor(@ColorInt int selectedBorderColor) { if (mSelectedBorderColor != selectedBorderColor) { mSelectedBorderColor = selectedBorderColor; if (mIsSelected) { invalidate(); } } } public void setSelectedBorderWidth(int selectedBorderWidth) { if (mSelectedBorderWidth != selectedBorderWidth) { mSelectedBorderWidth = selectedBorderWidth; if (mIsSelected) { invalidate(); } } } public void setSelectedMaskColor(@ColorInt int selectedMaskColor) { if (mSelectedMaskColor != selectedMaskColor) { mSelectedMaskColor = selectedMaskColor; if (mSelectedMaskColor != Color.TRANSPARENT) { mSelectedColorFilter = new PorterDuffColorFilter(mSelectedMaskColor, PorterDuff.Mode.DARKEN); } else { mSelectedColorFilter = null; } if (mIsSelected) { invalidate(); } } mSelectedMaskColor = selectedMaskColor; } public void setCircle(boolean isCircle) { if (mIsCircle != isCircle) { mIsCircle = isCircle; requestLayout(); invalidate(); } } public void setOval(boolean isOval) { boolean forceUpdate = false; if (isOval) { if (mIsCircle) { // 必须先取消圆形 mIsCircle = false; forceUpdate = true; } } if (mIsOval != isOval || forceUpdate) { mIsOval = isOval; requestLayout(); invalidate(); } } public int getBorderColor() { return mBorderColor; } public int getBorderWidth() { return mBorderWidth; } public int getCornerRadius() { return mCornerRadius; } public int getSelectedBorderColor() { return mSelectedBorderColor; } public int getSelectedBorderWidth() { return mSelectedBorderWidth; } public int getSelectedMaskColor() { return mSelectedMaskColor; } public boolean isCircle() { return mIsCircle; } public boolean isOval() { return !mIsCircle && mIsOval; } public boolean isSelected() { return mIsSelected; } public void setSelected(boolean isSelected) { if (mIsSelected != isSelected) { mIsSelected = isSelected; invalidate(); } } public void setTouchSelectModeEnabled(boolean touchSelectModeEnabled) { mIsTouchSelectModeEnabled = touchSelectModeEnabled; } public boolean isTouchSelectModeEnabled() { return mIsTouchSelectModeEnabled; } public void setSelectedColorFilter(ColorFilter cf) { if (mSelectedColorFilter == cf) { return; } mSelectedColorFilter = cf; if (mIsSelected) { invalidate(); } } @Override public void setColorFilter(ColorFilter cf) { if (mColorFilter == cf) { return; } mColorFilter = cf; if (!mIsSelected) { invalidate(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthSize, heightSize); return; } if (mIsCircle) { if (widthMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthSize, widthSize); } else if (heightMode == MeasureSpec.EXACTLY) { setMeasuredDimension(heightSize, heightSize); } else { if (mBitmap == null) { setMeasuredDimension(0, 0); } else { int w = Math.min(mBitmap.getWidth(), widthSize); int h = Math.min(mBitmap.getHeight(), heightSize); int size = Math.min(w, h); setMeasuredDimension(size, size); } } return; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); setupBitmap(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); setupBitmap(); } private Bitmap getBitmap() { Drawable drawable = getDrawable(); if (drawable == null) { return null; } if (drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); if (bitmap == null) { return null; } float bmWidth = bitmap.getWidth(), bmHeight = bitmap.getHeight(); if (bmWidth == 0 || bmHeight == 0) { return null; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // ensure minWidth and minHeight float minScaleX = getMinimumWidth() / bmWidth, minScaleY = getMinimumHeight() / bmHeight; if (minScaleX > 1 || minScaleY > 1) { float scale = Math.max(minScaleX, minScaleY); Matrix matrix = new Matrix(); matrix.postScale(scale, scale); return Bitmap.createBitmap( bitmap, 0, 0, (int) bmWidth, (int) bmHeight, matrix, false); } } return bitmap; } try { Bitmap bitmap; if (drawable instanceof ColorDrawable) { bitmap = Bitmap.createBitmap(COLOR_DRAWABLE_DIMEN, COLOR_DRAWABLE_DIMEN, BITMAP_CONFIG); } else { bitmap = Bitmap.createBitmap( drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); } Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } catch (Exception e) { e.printStackTrace(); return null; } } public void setupBitmap() { Bitmap bm = getBitmap(); if (bm == mBitmap) { return; } mBitmap = bm; if (mBitmap == null) { mBitmapShader = null; invalidate(); return; } mNeedResetShader = true; mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); if (mBitmapPaint == null) { mBitmapPaint = new Paint(); mBitmapPaint.setAntiAlias(true); } mBitmapPaint.setShader(mBitmapShader); requestLayout(); invalidate(); } private void updateBitmapShader() { mMatrix.reset(); mNeedResetShader = false; if (mBitmapShader == null || mBitmap == null) { return; } updateMatrix(mMatrix, mBitmap, mRectF); mBitmapShader.setLocalMatrix(mMatrix); mBitmapPaint.setShader(mBitmapShader); } private void updateMatrix(@NonNull Matrix matrix, @NonNull Bitmap bitmap, RectF drawRect) { final float bmWidth = bitmap.getWidth(); final float bmHeight = bitmap.getHeight(); final ScaleType scaleType = getScaleType(); if (scaleType == ScaleType.MATRIX) { updateScaleTypeMatrix(matrix, bitmap, drawRect); } else if (scaleType == ScaleType.CENTER) { float left = (mWidth - bmWidth) / 2; float top = (mHeight - bmHeight) / 2; matrix.postTranslate(left, top); drawRect.set( Math.max(0, left), Math.max(0, top), Math.min(left + bmWidth, mWidth), Math.min(top + bmHeight, mHeight)); } else if (scaleType == ScaleType.CENTER_CROP) { float scaleX = mWidth / bmWidth, scaleY = mHeight / bmHeight; final float scale = Math.max(scaleX, scaleY); matrix.setScale(scale, scale); matrix.postTranslate(-(scale * bmWidth - mWidth) / 2, -(scale * bmHeight - mHeight) / 2); drawRect.set(0, 0, mWidth, mHeight); } else if (scaleType == ScaleType.CENTER_INSIDE) { float scaleX = mWidth / bmWidth, scaleY = mHeight / bmHeight; if (scaleX >= 1 && scaleY >= 1) { float left = (mWidth - bmWidth) / 2; float top = (mHeight - bmHeight) / 2; matrix.postTranslate(left, top); drawRect.set(left, top, left + bmWidth, top + bmHeight); } else { float scale = Math.min(scaleX, scaleY); matrix.setScale(scale, scale); float bw = bmWidth * scale, bh = bmHeight * scale; float left = (mWidth - bw) / 2; float top = (mHeight - bh) / 2; matrix.postTranslate(left, top); drawRect.set(left, top, left + bw, top + bh); } } else if (scaleType == ScaleType.FIT_XY) { float scaleX = mWidth / bmWidth, scaleY = mHeight / bmHeight; matrix.setScale(scaleX, scaleY); drawRect.set(0, 0, mWidth, mHeight); } else { float scaleX = mWidth / bmWidth, scaleY = mHeight / bmHeight; float scale = Math.min(scaleX, scaleY); matrix.setScale(scale, scale); float bw = bmWidth * scale, bh = bmHeight * scale; if (scaleType == ScaleType.FIT_START) { drawRect.set(0, 0, bw, bh); } else if (scaleType == ScaleType.FIT_CENTER) { float left = (mWidth - bw) / 2; float top = (mHeight - bh) / 2; matrix.postTranslate(left, top); drawRect.set(left, top, left + bw, top + bh); } else { matrix.postTranslate(mWidth - bw, mHeight - bh); drawRect.set(mWidth - bw, mHeight - bh, mWidth, mHeight); } } } protected void updateScaleTypeMatrix(@NonNull Matrix matrix, @NonNull Bitmap bitmap, RectF drawRect) { matrix.set(getImageMatrix()); drawRect.set(0, 0, mWidth, mHeight); } private void drawBitmap(Canvas canvas, int borderWidth) { final float halfBorderWidth = borderWidth * 1.0f / 2; mBitmapPaint.setColorFilter(mIsSelected ? mSelectedColorFilter : mColorFilter); if (mIsCircle) { canvas.drawCircle(mRectF.centerX(), mRectF.centerY(), (Math.min(mRectF.width() / 2, mRectF.height() / 2)) - halfBorderWidth, mBitmapPaint); } else { mDrawRectF.left = mRectF.left + halfBorderWidth; //noinspection SuspiciousNameCombination mDrawRectF.top = mRectF.top + halfBorderWidth; mDrawRectF.right = mRectF.right - halfBorderWidth; mDrawRectF.bottom = mRectF.bottom - halfBorderWidth; if (mIsOval) { canvas.drawOval(mDrawRectF, mBitmapPaint); } else { canvas.drawRoundRect(mDrawRectF, mCornerRadius, mCornerRadius, mBitmapPaint); } } } private void drawBorder(Canvas canvas, int borderWidth) { if (borderWidth <= 0) { return; } final float halfBorderWidth = borderWidth * 1.0f / 2; mBorderPaint.setColor(mIsSelected ? mSelectedBorderColor : mBorderColor); mBorderPaint.setStrokeWidth(borderWidth); if (mIsCircle) { canvas.drawCircle(mRectF.centerX(), mRectF.centerY(), Math.min(mRectF.width(), mRectF.height()) / 2 - halfBorderWidth, mBorderPaint); } else { mDrawRectF.left = mRectF.left + halfBorderWidth; //noinspection SuspiciousNameCombination mDrawRectF.top = mRectF.top + halfBorderWidth; mDrawRectF.right = mRectF.right - halfBorderWidth; mDrawRectF.bottom = mRectF.bottom - halfBorderWidth; if (mIsOval) { canvas.drawOval(mDrawRectF, mBorderPaint); } else { canvas.drawRoundRect(mDrawRectF, mCornerRadius, mCornerRadius, mBorderPaint); } } } @Override protected void onDraw(Canvas canvas) { int width = getWidth(), height = getHeight(); if (width <= 0 || height <= 0) { return; } int borderWidth = mIsSelected ? mSelectedBorderWidth : mBorderWidth; if (mBitmap == null || mBitmapShader == null) { drawBorder(canvas, borderWidth); return; } if (mWidth != width || mHeight != height || mLastCalculateScaleType != getScaleType() || mNeedResetShader) { mWidth = width; mHeight = height; mLastCalculateScaleType = getScaleType(); updateBitmapShader(); } drawBitmap(canvas, borderWidth); drawBorder(canvas, borderWidth); } @Override public boolean onTouchEvent(MotionEvent event) { if (!this.isClickable()) { this.setSelected(false); return super.onTouchEvent(event); } if (!mIsTouchSelectModeEnabled) { return super.onTouchEvent(event); } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: this.setSelected(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_SCROLL: case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_CANCEL: this.setSelected(false); break; } return super.onTouchEvent(event); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIRadiusImageView2.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.util.AttributeSet; import android.view.MotionEvent; import androidx.annotation.ColorInt; import androidx.appcompat.widget.AppCompatImageView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaViewHelper; import com.qmuiteam.qmui.layout.IQMUILayout; import com.qmuiteam.qmui.layout.QMUILayoutHelper; /** * shown image in radius view, is different to {@link QMUIRadiusImageView} * the oval is not supported */ public class QMUIRadiusImageView2 extends AppCompatImageView implements IQMUILayout { private static final int DEFAULT_BORDER_COLOR = Color.GRAY; private QMUILayoutHelper mLayoutHelper; private QMUIAlphaViewHelper mAlphaViewHelper; private boolean mIsCircle = false; private boolean mIsSelected = false; private int mBorderWidth; private int mBorderColor; private int mSelectedBorderWidth; private int mSelectedBorderColor; private int mSelectedMaskColor; private boolean mIsTouchSelectModeEnabled = true; private ColorFilter mColorFilter; private ColorFilter mSelectedColorFilter; private boolean mIsInOnTouchEvent = false; public QMUIRadiusImageView2(Context context) { super(context); init(context, null, 0); } public QMUIRadiusImageView2(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIRadiusImageView2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); setChangeAlphaWhenPress(false); setChangeAlphaWhenDisable(false); TypedArray array = context.obtainStyledAttributes( attrs, R.styleable.QMUIRadiusImageView2, defStyleAttr, 0); mBorderWidth = array.getDimensionPixelSize(R.styleable.QMUIRadiusImageView2_qmui_border_width, 0); mBorderColor = array.getColor(R.styleable.QMUIRadiusImageView2_qmui_border_color, DEFAULT_BORDER_COLOR); mSelectedBorderWidth = array.getDimensionPixelSize( R.styleable.QMUIRadiusImageView2_qmui_selected_border_width, mBorderWidth); mSelectedBorderColor = array.getColor( R.styleable.QMUIRadiusImageView2_qmui_selected_border_color, mBorderColor); mSelectedMaskColor = array.getColor( R.styleable.QMUIRadiusImageView2_qmui_selected_mask_color, Color.TRANSPARENT); if (mSelectedMaskColor != Color.TRANSPARENT) { mSelectedColorFilter = new PorterDuffColorFilter(mSelectedMaskColor, PorterDuff.Mode.DARKEN); } mIsTouchSelectModeEnabled = array.getBoolean( R.styleable.QMUIRadiusImageView2_qmui_is_touch_select_mode_enabled, true); mIsCircle = array.getBoolean(R.styleable.QMUIRadiusImageView2_qmui_is_circle, false); if (!mIsCircle) { setRadius(array.getDimensionPixelSize( R.styleable.QMUIRadiusImageView2_qmui_corner_radius, 0)); } array.recycle(); mLayoutHelper.setBorderWidth(mBorderWidth); mLayoutHelper.setBorderColor(mBorderColor); } private QMUIAlphaViewHelper getAlphaViewHelper() { if (mAlphaViewHelper == null) { mAlphaViewHelper = new QMUIAlphaViewHelper(this); } return mAlphaViewHelper; } public void setCornerRadius(int cornerRadius) { setRadius(cornerRadius); } @Override protected boolean setFrame(int l, int t, int r, int b) { return super.setFrame(l, t, r, b); } @Override public void setPressed(boolean pressed) { super.setPressed(pressed); getAlphaViewHelper().onPressedChanged(this, pressed); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); getAlphaViewHelper().onEnabledChanged(this, enabled); } /** * 设置是否要在 press 时改变透明度 * * @param changeAlphaWhenPress 是否要在 press 时改变透明度 */ public void setChangeAlphaWhenPress(boolean changeAlphaWhenPress) { getAlphaViewHelper().setChangeAlphaWhenPress(changeAlphaWhenPress); } /** * 设置是否要在 disabled 时改变透明度 * * @param changeAlphaWhenDisable 是否要在 disabled 时改变透明度 */ public void setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable) { getAlphaViewHelper().setChangeAlphaWhenDisable(changeAlphaWhenDisable); } public void setCircle(boolean isCircle) { if (mIsCircle != isCircle) { mIsCircle = isCircle; requestLayout(); invalidate(); } } public int getBorderColor() { return mBorderColor; } public int getBorderWidth() { return mBorderWidth; } public int getCornerRadius() { return getRadius(); } public int getSelectedBorderColor() { return mSelectedBorderColor; } public int getSelectedBorderWidth() { return mSelectedBorderWidth; } public int getSelectedMaskColor() { return mSelectedMaskColor; } public boolean isCircle() { return mIsCircle; } public boolean isSelected() { return mIsSelected; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } if (mIsCircle) { int h = getMeasuredHeight(); int w = getMeasuredWidth(); int radius = w / 2; if (h != w) { int size = Math.min(h, w); radius = size / 2; int measureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); super.onMeasure(measureSpec, measureSpec); } setRadius(radius); } } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setBorderColor(@ColorInt int borderColor) { if (mBorderColor != borderColor) { mBorderColor = borderColor; if (!mIsSelected) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } } } @Override public void setBorderWidth(int borderWidth) { if (mBorderWidth != borderWidth) { mBorderWidth = borderWidth; if (!mIsSelected) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } } } public void setSelectedBorderColor(@ColorInt int selectedBorderColor) { if (mSelectedBorderColor != selectedBorderColor) { mSelectedBorderColor = selectedBorderColor; if (mIsSelected) { mLayoutHelper.setBorderColor(selectedBorderColor); invalidate(); } } } public void setSelectedBorderWidth(int selectedBorderWidth) { if (mSelectedBorderWidth != selectedBorderWidth) { mSelectedBorderWidth = selectedBorderWidth; if (mIsSelected) { mLayoutHelper.setBorderWidth(selectedBorderWidth); invalidate(); } } } public void setSelectedMaskColor(@ColorInt int selectedMaskColor) { if (mSelectedMaskColor != selectedMaskColor) { mSelectedMaskColor = selectedMaskColor; if (mSelectedMaskColor != Color.TRANSPARENT) { mSelectedColorFilter = new PorterDuffColorFilter(mSelectedMaskColor, PorterDuff.Mode.DARKEN); } else { mSelectedColorFilter = null; } if (mIsSelected) { invalidate(); } } mSelectedMaskColor = selectedMaskColor; } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public void setSelected(boolean selected) { if (!mIsInOnTouchEvent) { super.setSelected(selected); } if (mIsSelected != selected) { mIsSelected = selected; if (mIsSelected) { super.setColorFilter(mSelectedColorFilter); } else { super.setColorFilter(mColorFilter); } int borderWidth = mIsSelected ? mSelectedBorderWidth : mBorderWidth; int borderColor = mIsSelected ? mSelectedBorderColor : mBorderColor; mLayoutHelper.setBorderWidth(borderWidth); mLayoutHelper.setBorderColor(borderColor); invalidate(); } } public void setTouchSelectModeEnabled(boolean touchSelectModeEnabled) { mIsTouchSelectModeEnabled = touchSelectModeEnabled; } public boolean isTouchSelectModeEnabled() { return mIsTouchSelectModeEnabled; } public void setSelectedColorFilter(ColorFilter cf) { if (mSelectedColorFilter == cf) { return; } mSelectedColorFilter = cf; if (mIsSelected) { super.setColorFilter(cf); } } @Override public void setColorFilter(ColorFilter cf) { if (mColorFilter == cf) { return; } mColorFilter = cf; if (!mIsSelected) { super.setColorFilter(cf); } } @Override public boolean onTouchEvent(MotionEvent event) { if (!this.isClickable()) { return super.onTouchEvent(event); } else if (mIsTouchSelectModeEnabled) { mIsInOnTouchEvent = true; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: this.setSelected(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_SCROLL: case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_CANCEL: this.setSelected(false); break; } mIsInOnTouchEvent = false; } return super.onTouchEvent(event); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISeekBar.java ================================================ package com.qmuiteam.qmui.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; public class QMUISeekBar extends QMUISlider { private int mTickHeight; private int mTickWidth; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(2); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_seek_bar_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.PROGRESS_COLOR, R.attr.qmui_skin_support_seek_bar_color); } public QMUISeekBar(@NonNull Context context) { this(context, null); } public QMUISeekBar(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.QMUISeekBarStyle); } public QMUISeekBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.QMUISeekBar, defStyleAttr, 0); mTickWidth = array.getDimensionPixelSize(R.styleable.QMUISeekBar_qmui_seek_bar_tick_width, QMUIDisplayHelper.dp2px(context, 1)); mTickHeight = array.getDimensionPixelSize(R.styleable.QMUISeekBar_qmui_seek_bar_tick_height, QMUIDisplayHelper.dp2px(context, 4)); array.recycle(); setClickToChangeProgress(true); } public void setTickHeight(int tickHeight) { mTickHeight = tickHeight; invalidate(); } public void setTickWidth(int tickWidth) { mTickWidth = tickWidth; invalidate(); } public int getTickHeight() { return mTickHeight; } @Override protected void drawRect(Canvas canvas, RectF rect, int barHeight, Paint paint, boolean forProgress) { canvas.drawRect(rect, paint); } @Override protected void drawTick(Canvas canvas, int currentTickCount, int totalTickCount, int left, int right, float y, Paint paint, int barNormalColor, int barProgressColor) { if (mTickHeight <= 0 || mTickWidth <= 0 || totalTickCount < 1) { return; } float step = ((float) (right - left - mTickWidth)) / totalTickCount; float t = y - mTickHeight / 2f; float b = y + mTickHeight / 2f; float l, r; float x = left + mTickWidth / 2f; for (int i = 0; i <= totalTickCount; i++) { l = x - mTickWidth / 2f; r = x + mTickWidth / 2f; paint.setColor(i <= currentTickCount ? barProgressColor : barNormalColor); paint.setStyle(Paint.Style.FILL); canvas.drawRect(l, t, r, b, paint); x += step; } } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUISlider.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.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUILayoutHelper; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; public class QMUISlider extends FrameLayout implements IQMUISkinDefaultAttrProvider { public static final int PROGRESS_NOT_SET = -1; private Paint mBarPaint; private int mBarHeight; private int mBarNormalColor; private int mBarProgressColor; private int mRecordProgressColor; private boolean mConstraintThumbInMoving = true; private Callback mCallback; private IThumbView mThumbView; private QMUIViewOffsetHelper mThumbViewOffsetHelper; private int mTickCount; private int mCurrentProgress = 0; private boolean mIsProgressFirstSet = false; private boolean mClickToChangeProgress = false; private boolean mLongTouchToChangeProgress = false; private int mRecordProgress = PROGRESS_NOT_SET; private int mDownTouchX = 0; private int mLastTouchX = 0; private boolean mIsThumbTouched = false; private boolean mIsMoving = false; private int mTouchSlop; private RectF mTempRect = new RectF(); private LongPressAction mLongPressAction = new LongPressAction(); private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(2); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_slider_bar_bg_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.PROGRESS_COLOR, R.attr.qmui_skin_support_slider_bar_progress_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.HINT_COLOR, R.attr.qmui_skin_support_slider_record_progress_color); } public QMUISlider(@NonNull Context context) { this(context, null); } public QMUISlider(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.QMUISliderStyle); } public QMUISlider(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.QMUISlider, defStyleAttr, 0); mBarHeight = array.getDimensionPixelSize(R.styleable.QMUISlider_qmui_slider_bar_height, QMUIDisplayHelper.dp2px(context, 2)); mBarNormalColor = array.getColor(R.styleable.QMUISlider_qmui_slider_bar_normal_color, Color.WHITE); mBarProgressColor = array.getColor(R.styleable.QMUISlider_qmui_slider_bar_progress_color, Color.BLUE); mRecordProgressColor = array.getColor(R.styleable.QMUISlider_qmui_slider_bar_record_progress_color, Color.GRAY); mTickCount = array.getInt(R.styleable.QMUISlider_qmui_slider_bar_tick_count, 100); mConstraintThumbInMoving = array.getBoolean(R.styleable.QMUISlider_qmui_slider_bar_constraint_thumb_in_moving, true); int thumbSize = array.getDimensionPixelSize( R.styleable.QMUISlider_qmui_slider_bar_thumb_size, QMUIDisplayHelper.dp2px(getContext(), 24)); int thumbStyleAttr = 0; String thumbStyleAttrString = array.getString(R.styleable.QMUISlider_qmui_slider_bar_thumb_style_attr); if (thumbStyleAttrString != null) { thumbStyleAttr = getResources().getIdentifier( thumbStyleAttrString, "attr", context.getPackageName()); } boolean useClipChildrenByDeveloper = array.getBoolean( R.styleable.QMUISlider_qmui_slider_bar_use_clip_children_by_developer, false); if (!useClipChildrenByDeveloper) { int paddingHor = array.getDimensionPixelOffset( R.styleable.QMUISlider_qmui_slider_bar_padding_hor_for_thumb_shadow, 0); int paddingVer = array.getDimensionPixelOffset( R.styleable.QMUISlider_qmui_slider_bar_padding_ver_for_thumb_shadow, 0); setPadding(paddingHor, paddingVer, paddingHor, paddingVer); } array.recycle(); mBarPaint = new Paint(); mBarPaint.setStyle(Paint.Style.FILL); mBarPaint.setAntiAlias(true); mTouchSlop = QMUIDisplayHelper.dp2px(context, 2); setWillNotDraw(false); setClipToPadding(false); setClipChildren(false); IThumbView thumbView = onCreateThumbView(context, thumbSize, thumbStyleAttr); if (!(thumbView instanceof View)) { throw new IllegalArgumentException("thumbView must be a instance of View"); } mThumbView = thumbView; View thumbAsView = (View) thumbView; mThumbViewOffsetHelper = new QMUIViewOffsetHelper(thumbAsView); addView(thumbAsView, onCreateThumbLayoutParams()); thumbView.render(mCurrentProgress, mTickCount); } public void setCallback(Callback callback) { mCallback = callback; } public void setCurrentProgress(int currentProgress) { if (!mIsMoving) { int progress = QMUILangHelper.constrain(currentProgress, 0, mTickCount); if (mCurrentProgress != progress || !mIsProgressFirstSet) { mIsProgressFirstSet = true; safeSetCurrentProgress(progress); if (mCallback != null) { mCallback.onProgressChange(this, progress, mTickCount, false); } invalidate(); } } } public void setRecordProgress(int recordProgress) { if (recordProgress != mRecordProgress) { if (recordProgress != PROGRESS_NOT_SET) { recordProgress = QMUILangHelper.constrain(recordProgress, 0, mTickCount); } mRecordProgress = recordProgress; invalidate(); } } public int getCurrentProgress() { return mCurrentProgress; } public void setTickCount(int tickCount) { if (mTickCount != tickCount) { mTickCount = tickCount; setCurrentProgress(QMUILangHelper.constrain(mCurrentProgress, 0, mTickCount)); mThumbView.render(mCurrentProgress, mTickCount); invalidate(); } } public int getTickCount() { return mTickCount; } public void setThumbSkin(QMUISkinValueBuilder valueBuilder) { QMUISkinHelper.setSkinValue(convertThumbToView(), valueBuilder); } private void safeSetCurrentProgress(int currentProgress) { mCurrentProgress = currentProgress; mThumbView.render(currentProgress, mTickCount); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getMeasuredHeight() < mBarHeight) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( mBarHeight + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY)); } } @Override protected final void onLayout(boolean changed, int left, int top, int right, int bottom) { onLayoutCustomChildren(changed, left, top, right, bottom); View thumbView = convertThumbToView(); int paddingTop = getPaddingTop(), thumbHeight = thumbView.getMeasuredHeight(), thumbWidth = thumbView.getMeasuredWidth(); int l = getPaddingLeft() + mThumbView.getLeftRightMargin(); int t = paddingTop + (bottom - top - paddingTop - getPaddingBottom() - thumbView.getMeasuredHeight()) / 2; thumbView.layout(l, t, l + thumbWidth, t + thumbHeight); mThumbViewOffsetHelper.onViewLayout(); } protected void onLayoutCustomChildren(boolean changed, int left, int top, int right, int bottom) { } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { mDownTouchX = (int) event.getX(); mLastTouchX = mDownTouchX; mIsThumbTouched = isThumbTouched(event.getX(), event.getY()); if (mIsThumbTouched) { mThumbView.setPress(true); }else if(mLongTouchToChangeProgress){ removeCallbacks(mLongPressAction); postOnAnimationDelayed(mLongPressAction, 300); } if (mCallback != null) { mCallback.onTouchDown(this, mCurrentProgress, mTickCount, mIsThumbTouched); } } else if (action == MotionEvent.ACTION_MOVE) { int x = (int) event.getX(); int dx = x - mLastTouchX; mLastTouchX = x; if (!mIsMoving && mIsThumbTouched) { if (Math.abs(mLastTouchX - mDownTouchX) > mTouchSlop) { removeCallbacks(mLongPressAction); mIsMoving = true; if (mCallback != null) { mCallback.onStartMoving(this, mCurrentProgress, mTickCount); } if (dx > 0) { dx -= mTouchSlop; } else { dx += mTouchSlop; } } } if (mIsMoving) { QMUIViewHelper.safeRequestDisallowInterceptTouchEvent(this, true); int maxOffset = getMaxThumbOffset(); int oldProgress = mCurrentProgress; if (mConstraintThumbInMoving) { checkTouch(x, maxOffset); } else { mThumbViewOffsetHelper.setLeftAndRightOffset( QMUILangHelper.constrain( mThumbViewOffsetHelper.getLeftAndRightOffset() + dx, 0, maxOffset) ); calculateByThumbPosition(maxOffset); } if (mCallback != null && oldProgress != mCurrentProgress) { mCallback.onProgressChange(this, mCurrentProgress, mTickCount, true); } invalidate(); } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { removeCallbacks(mLongPressAction); mLastTouchX = -1; QMUIViewHelper.safeRequestDisallowInterceptTouchEvent(this, false); if (mIsMoving) { mIsMoving = false; if (mCallback != null) { mCallback.onStopMoving(this, mCurrentProgress, mTickCount); } } if (mIsThumbTouched) { mIsThumbTouched = false; mThumbView.setPress(false); } else if (action == MotionEvent.ACTION_UP) { int x = (int) event.getX(); boolean isRecordProgressClicked = isRecordProgressClicked(x); if (Math.abs(x - mDownTouchX) < mTouchSlop && (mClickToChangeProgress || isRecordProgressClicked)) { int oldProgress = mCurrentProgress; if (isRecordProgressClicked) { safeSetCurrentProgress(mRecordProgress); } else { checkTouch(x, getMaxThumbOffset()); } invalidate(); if (mCallback != null && oldProgress != mCurrentProgress) { mCallback.onProgressChange(this, mCurrentProgress, mTickCount, true); } } } if (mCallback != null) { mCallback.onTouchUp(this, mCurrentProgress, mTickCount); } } else { removeCallbacks(mLongPressAction); } return true; } private void checkTouch(int touchX, int maxOffset) { if(mThumbView == null){ return; } int moveX = touchX - getPaddingLeft() - mThumbView.getLeftRightMargin(); float step = (float) maxOffset / mTickCount; if (moveX <= step / 2) { mThumbViewOffsetHelper.setLeftAndRightOffset(0); safeSetCurrentProgress(0); } else if (touchX >= getWidth() - getPaddingRight() - mThumbView.getLeftRightMargin() - step / 2) { mThumbViewOffsetHelper.setLeftAndRightOffset(maxOffset); safeSetCurrentProgress(mTickCount); } else { float percent = (float) moveX / (getWidth() - getPaddingLeft() - getPaddingRight() - 2 * mThumbView.getLeftRightMargin()); int target = (int) (mTickCount * percent + 0.5f); mThumbViewOffsetHelper.setLeftAndRightOffset((int) (target * step)); safeSetCurrentProgress(target); } } public void setClickToChangeProgress(boolean clickToChangeProgress) { mClickToChangeProgress = clickToChangeProgress; } public void setLongTouchToChangeProgress(boolean longTouchToChangeProgress) { mLongTouchToChangeProgress = longTouchToChangeProgress; } public boolean isLongTouchToChangeProgress() { return mLongTouchToChangeProgress; } public boolean isClickToChangeProgress() { return mClickToChangeProgress; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int l = getPaddingLeft(); int r = getWidth() - getPaddingRight(); int bt = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom() - mBarHeight) / 2; int bb = bt + mBarHeight; mBarPaint.setColor(mBarNormalColor); mTempRect.set(l, bt, r, bb); drawRect(canvas, mTempRect, mBarHeight, mBarPaint, false); float step = (float) getMaxThumbOffset() / mTickCount; int progressOffset = (int) (step * mCurrentProgress); mBarPaint.setColor(mBarProgressColor); View thumb = convertThumbToView(); if (thumb != null && thumb.getVisibility() == View.VISIBLE) { if (!mIsMoving) { mThumbViewOffsetHelper.setLeftAndRightOffset(progressOffset); } mTempRect.set(l, bt, (thumb.getRight() + thumb.getLeft()) / 2f, bb); drawRect(canvas, mTempRect, mBarHeight, mBarPaint, true); } else { mTempRect.set(l, bt, l + progressOffset, bb); drawRect(canvas, mTempRect, mBarHeight, mBarPaint, true); } drawTick(canvas, mCurrentProgress, mTickCount, l, r, mTempRect.centerY(), mBarPaint, mBarNormalColor, mBarProgressColor); if (mRecordProgress != PROGRESS_NOT_SET && thumb != null) { mBarPaint.setColor(mRecordProgressColor); float recordPos = getPaddingLeft() + mThumbView.getLeftRightMargin() + (int) (step * mRecordProgress); mTempRect.set(recordPos, thumb.getTop(), recordPos + thumb.getWidth(), thumb.getBottom()); drawRecordProgress(canvas, mTempRect, mBarPaint); } } protected void drawRect(Canvas canvas, RectF rect, int barHeight, Paint paint, boolean forProgress) { int radius = barHeight / 2; canvas.drawRoundRect(rect, radius, radius, paint); } protected void drawRecordProgress(Canvas canvas, RectF rect, Paint paint) { float radius = rect.height() / 2; canvas.drawRoundRect(rect, radius, radius, paint); } protected void drawTick(Canvas canvas, int currentTickCount, int totalTickCount, int left, int right, float y, Paint paint, int barNormalColor, int barProgressColor) { } public void setBarHeight(int barHeight) { if (mBarHeight != barHeight) { mBarHeight = barHeight; requestLayout(); } } public int getBarHeight() { return mBarHeight; } public void setBarNormalColor(int barNormalColor) { if (mBarNormalColor != barNormalColor) { mBarNormalColor = barNormalColor; invalidate(); } } public int getBarNormalColor() { return mBarNormalColor; } public void setBarProgressColor(int barProgressColor) { if (mBarProgressColor != barProgressColor) { mBarProgressColor = barProgressColor; invalidate(); } } public int getBarProgressColor() { return mBarProgressColor; } public void setRecordProgressColor(int recordProgressColor) { if (mRecordProgressColor != recordProgressColor) { mRecordProgressColor = recordProgressColor; invalidate(); } } public int getRecordProgressColor() { return mRecordProgressColor; } public int getRecordProgress() { return mRecordProgress; } public void setConstraintThumbInMoving(boolean constraintThumbInMoving) { mConstraintThumbInMoving = constraintThumbInMoving; } private void calculateByThumbPosition(int maxOffset) { View thumbView = convertThumbToView(); float percent = mThumbViewOffsetHelper.getLeftAndRightOffset() * 1f / maxOffset; safeSetCurrentProgress(QMUILangHelper.constrain( (int) (mTickCount * percent + 0.5f), 0, mTickCount )); } @NonNull protected IThumbView onCreateThumbView(Context context, int thumbSize, int thumbStyleAttr) { return new DefaultThumbView(context, thumbSize, thumbStyleAttr); } protected LayoutParams onCreateThumbLayoutParams() { return new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } private View convertThumbToView() { return (View) mThumbView; } private boolean isThumbTouched(float x, float y) { return isThumbViewTouched(convertThumbToView(), x, y); } protected boolean isThumbViewTouched(View thumbView, float x, float y) { return thumbView.getVisibility() == View.VISIBLE && thumbView.getLeft() <= x && thumbView.getRight() >= x && thumbView.getTop() <= y && thumbView.getBottom() >= y; } protected boolean isRecordProgressClicked(int x) { if (mRecordProgress == PROGRESS_NOT_SET) { return false; } View thumbView = convertThumbToView(); float percent = mRecordProgress * 1f / mTickCount; float left = (getWidth() - getPaddingLeft() - getPaddingRight()) * percent - thumbView.getWidth() / 2f; float right = left + thumbView.getWidth(); return x >= left && x <= right; } private int getMaxThumbOffset() { return getWidth() - getPaddingLeft() - getPaddingRight() - mThumbView.getLeftRightMargin() * 2 - convertThumbToView().getWidth(); } public interface IThumbView { void render(int progress, int tickCount); void setPress(boolean isPressed); int getLeftRightMargin(); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } public interface Callback { void onProgressChange(QMUISlider slider, int progress, int tickCount, boolean fromUser); void onTouchDown(QMUISlider slider, int progress, int tickCount, boolean hitThumb); void onTouchUp(QMUISlider slider, int progress, int tickCount); void onStartMoving(QMUISlider slider, int progress, int tickCount); void onStopMoving(QMUISlider slider, int progress, int tickCount); void onLongTouch(QMUISlider slider, int progress, int tickCount); } public static class DefaultCallback implements Callback { @Override public void onProgressChange(QMUISlider slider, int progress, int tickCount, boolean fromUser) { } @Override public void onTouchDown(QMUISlider slider, int progress, int tickCount, boolean hitThumb) { } @Override public void onTouchUp(QMUISlider slider, int progress, int tickCount) { } @Override public void onStartMoving(QMUISlider slider, int progress, int tickCount) { } @Override public void onStopMoving(QMUISlider slider, int progress, int tickCount) { } @Override public void onLongTouch(QMUISlider slider, int progress, int tickCount) { } } public static class DefaultThumbView extends View implements IThumbView, IQMUISkinDefaultAttrProvider { private final QMUILayoutHelper mLayoutHelper; private final int mSize; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(2); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_slider_thumb_bg_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BORDER, R.attr.qmui_skin_support_slider_thumb_border_color); } public DefaultThumbView(Context context, int size, int defAttr) { super(context, null, defAttr); mSize = size; mLayoutHelper = new QMUILayoutHelper(context, null, defAttr, this); mLayoutHelper.setRadius(size / 2); setPress(false); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } public void setBorderColor(int color) { mLayoutHelper.setBorderColor(color); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mSize, mSize); } @Override public void render(int progress, int tickCount) { } @Override public void setPress(boolean isPressed) { } @Override public int getLeftRightMargin() { return 0; } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } } class LongPressAction implements Runnable { @Override public void run() { mIsMoving = true; int oldProgress = mCurrentProgress; checkTouch(mLastTouchX, getMaxThumbOffset()); mIsThumbTouched = true; mThumbView.setPress(true); if (mCallback != null && oldProgress != mCurrentProgress) { mCallback.onLongTouch(QMUISlider.this, mCurrentProgress, mTickCount); } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBar.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.widget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.Button; import android.widget.LinearLayout; import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaImageButton; import com.qmuiteam.qmui.layout.QMUIRelativeLayout; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.IQMUISkinHandlerView; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.skin.defaultAttr.QMUISkinSimpleDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; /** * A standard toolbar for use within application content. *

*

    *
  • add icon/text/custom-view in left or right.
  • *
  • set title and subtitle with gravity support.
  • *
*/ public class QMUITopBar extends QMUIRelativeLayout implements IQMUISkinHandlerView, IQMUISkinDefaultAttrProvider { private static final int DEFAULT_VIEW_ID = -1; private int mLeftLastViewId; // 左侧最右 view 的 id private int mRightLastViewId; // 右侧最左 view 的 id private View mCenterView; // 中间的 View private LinearLayout mTitleContainerView; // 包裹 title 和 subTitle 的容器 private QMUIQQFaceView mTitleView; // 显示 title 文字的 TextView private QMUISpanTouchFixTextView mSubTitleView; // 显示 subTitle 文字的 TextView private List mLeftViewList; private List mRightViewList; private int mTitleGravity; private int mLeftBackDrawableRes; private int mLeftBackViewWidth; private boolean mClearLeftPaddingWhenAddLeftBackView; private int mTitleTextSize; private Typeface mTitleTypeface; private Typeface mSubTitleTypeface; private int mTitleTextSizeWithSubTitle; private int mSubTitleTextSize; private int mTitleTextColor; private int mSubTitleTextColor; private int mTitleMarginHorWhenNoBtnAside; private int mTitleContainerPaddingHor; private int mTopBarImageBtnWidth; private int mTopBarImageBtnHeight; private int mTopBarTextBtnPaddingHor; private ColorStateList mTopBarTextBtnTextColor; private int mTopBarTextBtnTextSize; private Typeface mTopBarTextBtnTypeface; private int mTopBarHeight = -1; private Rect mTitleContainerRect; private boolean mIsBackgroundSetterDisabled = false; private TruncateAt mEllipsize; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(4); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BOTTOM_SEPARATOR, R.attr.qmui_skin_support_topbar_separator_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_topbar_bg); } public QMUITopBar(Context context) { this(context, null); } public QMUITopBar(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUITopBarStyle); } public QMUITopBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initVar(); init(context, attrs, defStyleAttr); } private void initVar() { mLeftLastViewId = DEFAULT_VIEW_ID; mRightLastViewId = DEFAULT_VIEW_ID; mLeftViewList = new ArrayList<>(); mRightViewList = new ArrayList<>(); } void init(Context context, AttributeSet attrs) { init(context, attrs, R.attr.QMUITopBarStyle); } void init(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.QMUITopBar, defStyleAttr, 0); mLeftBackDrawableRes = array.getResourceId(R.styleable.QMUITopBar_qmui_topbar_left_back_drawable_id, R.drawable.qmui_icon_topbar_back); mLeftBackViewWidth = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_left_back_width, -1); mClearLeftPaddingWhenAddLeftBackView = array.getBoolean(R.styleable.QMUITopBar_qmui_topbar_clear_left_padding_when_add_left_back_view, false); mTitleGravity = array.getInt(R.styleable.QMUITopBar_qmui_topbar_title_gravity, Gravity.CENTER); mTitleTextSize = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_title_text_size, QMUIDisplayHelper.sp2px(context, 17)); mTitleTextSizeWithSubTitle = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_title_text_size_with_subtitle, QMUIDisplayHelper.sp2px(context, 16)); mSubTitleTextSize = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_subtitle_text_size, QMUIDisplayHelper.sp2px(context, 11)); mTitleTextColor = array.getColor(R.styleable.QMUITopBar_qmui_topbar_title_color, QMUIResHelper.getAttrColor(context, R.attr.qmui_config_color_gray_1)); mSubTitleTextColor = array.getColor(R.styleable.QMUITopBar_qmui_topbar_subtitle_color, QMUIResHelper.getAttrColor(context, R.attr.qmui_config_color_gray_4)); mTitleMarginHorWhenNoBtnAside = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_title_margin_horizontal_when_no_btn_aside, 0); mTitleContainerPaddingHor = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_title_container_padding_horizontal, 0); mTopBarImageBtnWidth = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_image_btn_width, QMUIDisplayHelper.dp2px(context, 48)); mTopBarImageBtnHeight = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_image_btn_height, QMUIDisplayHelper.dp2px(context, 48)); mTopBarTextBtnPaddingHor = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_text_btn_padding_horizontal, QMUIDisplayHelper.dp2px(context, 12)); mTopBarTextBtnTextColor = array.getColorStateList(R.styleable.QMUITopBar_qmui_topbar_text_btn_color_state_list); mTopBarTextBtnTextSize = array.getDimensionPixelSize(R.styleable.QMUITopBar_qmui_topbar_text_btn_text_size, QMUIDisplayHelper.sp2px(context, 16)); mTitleTypeface = array.getBoolean(R.styleable.QMUITopBar_qmui_topbar_title_bold, false) ? Typeface.DEFAULT_BOLD : null; mSubTitleTypeface = array.getBoolean(R.styleable.QMUITopBar_qmui_topbar_subtitle_bold, false) ? Typeface.DEFAULT_BOLD : null; mTopBarTextBtnTypeface = array.getBoolean(R.styleable.QMUITopBar_qmui_topbar_text_btn_bold, false) ? Typeface.DEFAULT_BOLD : null; int ellipsize = array.getInt(R.styleable.QMUITopBar_android_ellipsize, -1) ; switch (ellipsize) { case 1: mEllipsize = TextUtils.TruncateAt.START; break; case 2: mEllipsize = TextUtils.TruncateAt.MIDDLE; break; case 3: mEllipsize = TextUtils.TruncateAt.END; break; default: mEllipsize = null; break; } array.recycle(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); ViewParent parent = getParent(); while (parent instanceof View) { if (parent instanceof QMUICollapsingTopBarLayout) { makeSureTitleContainerView(); return; } parent = parent.getParent(); } } /** * 在 TopBar 的中间添加 View,如果此前已经有 View 通过该方法添加到 TopBar,则旧的View会被 remove * * @param view 要添加到TopBar中间的View */ public void setCenterView(View view) { if (mCenterView == view) { return; } if (mCenterView != null) { removeView(mCenterView); } mCenterView = view; LayoutParams params = (LayoutParams) mCenterView.getLayoutParams(); if (params == null) { params = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } params.addRule(RelativeLayout.CENTER_IN_PARENT); addView(view, params); } /** * 添加 TopBar 的标题 * * @param resId TopBar 的标题 resId */ public QMUIQQFaceView setTitle(int resId) { return setTitle(getContext().getString(resId)); } /** * 添加 TopBar 的标题 * * @param title TopBar 的标题 */ public QMUIQQFaceView setTitle(String title) { QMUIQQFaceView titleView = ensureTitleView(); titleView.setText(title); if (QMUILangHelper.isNullOrEmpty(title)) { titleView.setVisibility(GONE); } else { titleView.setVisibility(VISIBLE); } return titleView; } public CharSequence getTitle() { if (mTitleView == null) { return null; } return mTitleView.getText(); } @Nullable public QMUIQQFaceView getTitleView(){ return mTitleView; } public void showTitleView(boolean toShow) { if (mTitleView != null) { mTitleView.setVisibility(toShow ? VISIBLE : GONE); } } private QMUIQQFaceView ensureTitleView() { if (mTitleView == null) { mTitleView = new QMUIQQFaceView(getContext()); mTitleView.setGravity(Gravity.CENTER); mTitleView.setSingleLine(true); mTitleView.setEllipsize(mEllipsize); mTitleView.setTypeface(mTitleTypeface); mTitleView.setTextColor(mTitleTextColor); QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr(QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_topbar_title_color); mTitleView.setTag(R.id.qmui_skin_default_attr_provider, provider); updateTitleViewStyle(); LinearLayout.LayoutParams titleLp = generateTitleViewAndSubTitleViewLp(); makeSureTitleContainerView().addView(mTitleView, titleLp); } return mTitleView; } /** * 更新 titleView 的样式(因为有没有 subTitle 会影响 titleView 的样式) */ private void updateTitleViewStyle() { if (mTitleView != null) { if (mSubTitleView == null || QMUILangHelper.isNullOrEmpty(mSubTitleView.getText())) { mTitleView.setTextSize(mTitleTextSize); } else { mTitleView.setTextSize(mTitleTextSizeWithSubTitle); } } } /** * 添加 TopBar 的副标题 * * @param subTitle TopBar 的副标题 */ public QMUISpanTouchFixTextView setSubTitle(CharSequence subTitle) { QMUISpanTouchFixTextView subTitleView = ensureSubTitleView(); subTitleView.setText(subTitle); if (QMUILangHelper.isNullOrEmpty(subTitle)) { subTitleView.setVisibility(GONE); } else { subTitleView.setVisibility(VISIBLE); } // 更新 titleView 的样式(因为有没有 subTitle 会影响 titleView 的样式) updateTitleViewStyle(); return subTitleView; } /** * 添加 TopBar 的副标题 * * @param resId TopBar 的副标题 resId */ public QMUISpanTouchFixTextView setSubTitle(int resId) { return setSubTitle(getResources().getString(resId)); } private QMUISpanTouchFixTextView ensureSubTitleView() { if (mSubTitleView == null) { mSubTitleView = new QMUISpanTouchFixTextView(getContext()); mSubTitleView.setGravity(Gravity.CENTER); mSubTitleView.setSingleLine(true); mSubTitleView.setTypeface(mSubTitleTypeface); mSubTitleView.setEllipsize(mEllipsize); mSubTitleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSubTitleTextSize); mSubTitleView.setTextColor(mSubTitleTextColor); QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr(QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_topbar_subtitle_color); mSubTitleView.setTag(R.id.qmui_skin_default_attr_provider, provider); LinearLayout.LayoutParams titleLp = generateTitleViewAndSubTitleViewLp(); titleLp.topMargin = QMUIDisplayHelper.dp2px(getContext(), 1); makeSureTitleContainerView().addView(mSubTitleView, titleLp); } return mSubTitleView; } @Nullable public QMUISpanTouchFixTextView getSubTitleView(){ return mSubTitleView; } /** * 设置 TopBar 的 gravity,用于控制 title 和 subtitle 的对齐方式 * * @param gravity 参考 {@link android.view.Gravity} */ public void setTitleGravity(int gravity) { mTitleGravity = gravity; if (mTitleView != null) { ((LinearLayout.LayoutParams) mTitleView.getLayoutParams()).gravity = gravity; if (gravity == Gravity.CENTER || gravity == Gravity.CENTER_HORIZONTAL) { mTitleView.setPadding(getPaddingLeft(), getPaddingTop(), getPaddingLeft(), getPaddingBottom()); } } if (mSubTitleView != null) { ((LinearLayout.LayoutParams) mSubTitleView.getLayoutParams()).gravity = gravity; } requestLayout(); } public Rect getTitleContainerRect() { if (mTitleContainerRect == null) { mTitleContainerRect = new Rect(); } if (mTitleContainerView == null) { mTitleContainerRect.set(0, 0, 0, 0); } else { QMUIViewHelper.getDescendantRect(this, mTitleContainerView, mTitleContainerRect); } return mTitleContainerRect; } public LinearLayout getTitleContainerView() { return mTitleContainerView; } void disableBackgroundSetter(){ mIsBackgroundSetterDisabled = true; super.setBackgroundDrawable(null); } @Override public void setBackgroundDrawable(Drawable background) { if(!mIsBackgroundSetterDisabled){ super.setBackgroundDrawable(background); } } // ========================= leftView、rightView 相关的方法 private LinearLayout makeSureTitleContainerView() { if (mTitleContainerView == null) { mTitleContainerView = new LinearLayout(getContext()); // 垂直,后面要支持水平的话可以加个接口来设置 mTitleContainerView.setOrientation(LinearLayout.VERTICAL); mTitleContainerView.setGravity(Gravity.CENTER); mTitleContainerView.setPadding(mTitleContainerPaddingHor, 0, mTitleContainerPaddingHor, 0); addView(mTitleContainerView, generateTitleContainerViewLp()); } return mTitleContainerView; } /** * 生成 TitleContainerView 的 LayoutParams。 * 左右有按钮时,该 View 在左右按钮之间; * 没有左右按钮时,该 View 距离 TopBar 左右边缘有固定的距离 */ private LayoutParams generateTitleContainerViewLp() { return new LayoutParams(LayoutParams.MATCH_PARENT, QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_topbar_height)); } /** * 生成 titleView 或 subTitleView 的 LayoutParams */ private LinearLayout.LayoutParams generateTitleViewAndSubTitleViewLp() { LinearLayout.LayoutParams titleLp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); // 垂直居中 titleLp.gravity = mTitleGravity; return titleLp; } /** * 在TopBar的左侧添加View,如果此前已经有View通过该方法添加到TopBar,则新添加进去的View会出现在已有View的右侧 * * @param view 要添加到 TopBar 左边的 View * @param viewId 该按钮的id,可在ids.xml中找到合适的或新增。手工指定viewId是为了适应自动化测试。 */ public void addLeftView(View view, int viewId) { ViewGroup.LayoutParams viewLayoutParams = view.getLayoutParams(); LayoutParams layoutParams; if (viewLayoutParams != null && viewLayoutParams instanceof LayoutParams) { layoutParams = (LayoutParams) viewLayoutParams; } else { layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } this.addLeftView(view, viewId, layoutParams); } /** * 在TopBar的左侧添加View,如果此前已经有View通过该方法添加到TopBar,则新添加进去的View会出现在已有View的右侧。 * * @param view 要添加到 TopBar 左边的 View。 * @param viewId 该按钮的 id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @param layoutParams 传入一个 LayoutParams,当把 Button addView 到 TopBar 时,使用这个 LayouyParams。 */ public void addLeftView(View view, int viewId, LayoutParams layoutParams) { if (mLeftLastViewId == DEFAULT_VIEW_ID) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); } else { layoutParams.addRule(RelativeLayout.RIGHT_OF, mLeftLastViewId); } layoutParams.alignWithParent = true; // alignParentIfMissing mLeftLastViewId = viewId; view.setId(viewId); mLeftViewList.add(view); addView(view, layoutParams); } /** * 在 TopBar 的右侧添加 View,如果此前已经有 iew 通过该方法添加到 TopBar,则新添加进去的View会出现在已有View的左侧 * * @param view 要添加到 TopBar 右边的View * @param viewId 该按钮的id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 */ public void addRightView(View view, int viewId) { ViewGroup.LayoutParams viewLayoutParams = view.getLayoutParams(); LayoutParams layoutParams; if (viewLayoutParams != null && viewLayoutParams instanceof LayoutParams) { layoutParams = (LayoutParams) viewLayoutParams; } else { layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } this.addRightView(view, viewId, layoutParams); } /** * 在 TopBar 的右侧添加 View,如果此前已经有 View 通过该方法添加到 TopBar,则新添加进去的 View 会出现在已有View的左侧。 * * @param view 要添加到 TopBar 右边的 View。 * @param viewId 该按钮的 id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @param layoutParams 生成一个 LayoutParams,当把 Button addView 到 TopBar 时,使用这个 LayouyParams。 */ public void addRightView(View view, int viewId, LayoutParams layoutParams) { if (mRightLastViewId == DEFAULT_VIEW_ID) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); } else { layoutParams.addRule(RelativeLayout.LEFT_OF, mRightLastViewId); } layoutParams.alignWithParent = true; // alignParentIfMissing mRightLastViewId = viewId; view.setId(viewId); mRightViewList.add(view); addView(view, layoutParams); } public LayoutParams generateTopBarImageButtonLayoutParams(){ return generateTopBarImageButtonLayoutParams(-1, -1); } /** * 生成一个 LayoutParams,当把 Button addView 到 TopBar 时,使用这个 LayouyParams */ public LayoutParams generateTopBarImageButtonLayoutParams(int iconWidth, int iconHeight) { iconHeight = iconHeight > 0 ? iconHeight : mTopBarImageBtnHeight; LayoutParams lp = new LayoutParams(iconWidth > 0 ? iconWidth : mTopBarImageBtnWidth, iconHeight); lp.topMargin = Math.max(0, (getTopBarHeight() - iconHeight) / 2); return lp; } public QMUIAlphaImageButton addRightImageButton(int drawableResId, int viewId) { return addRightImageButton(drawableResId, true, viewId); } public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId) { return addRightImageButton(drawableResId, followTintColor, viewId, -1, -1); } /** * 根据 resourceId 生成一个 TopBar 的按钮,并 add 到 TopBar 的右侧 * * @param drawableResId 按钮图片的 resourceId * @param viewId 该按钮的 id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @param followTintColor 换肤时使用 tintColor 更改它的颜色 * @return 返回生成的按钮 */ public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { QMUIAlphaImageButton rightButton = generateTopBarImageButton(drawableResId, followTintColor); this.addRightView(rightButton, viewId, generateTopBarImageButtonLayoutParams(iconWidth, iconHeight)); return rightButton; } public QMUIAlphaImageButton addLeftImageButton(int drawableResId, int viewId) { return addLeftImageButton(drawableResId, true, viewId); } public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId) { return addLeftImageButton(drawableResId, followTintColor, viewId, -1, -1); } /** * 根据 resourceId 生成一个 TopBar 的按钮,并 add 到 TopBar 的左边 * * @param drawableResId 按钮图片的 resourceId * @param viewId 该按钮的 id,可在ids.xml中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @param followTintColor 换肤时使用 tintColor 更改它的颜色 * @return 返回生成的按钮 */ public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { QMUIAlphaImageButton leftButton = generateTopBarImageButton(drawableResId, followTintColor); this.addLeftView(leftButton, viewId, generateTopBarImageButtonLayoutParams(iconWidth, iconHeight)); return leftButton; } /** * 生成一个LayoutParams,当把 Button addView 到 TopBar 时,使用这个 LayouyParams */ public LayoutParams generateTopBarTextButtonLayoutParams() { LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, mTopBarImageBtnHeight); lp.topMargin = Math.max(0, (getTopBarHeight() - mTopBarImageBtnHeight) / 2); return lp; } /** * 在 TopBar 左边添加一个 Button,并设置文字 * * @param stringResId 按钮的文字的 resourceId * @param viewId 该按钮的id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @return 返回生成的按钮 */ public Button addLeftTextButton(int stringResId, int viewId) { return addLeftTextButton(getResources().getString(stringResId), viewId); } /** * 在 TopBar 左边添加一个 Button,并设置文字 * * @param buttonText 按钮的文字 * @param viewId 该按钮的 id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @return 返回生成的按钮 */ public Button addLeftTextButton(String buttonText, int viewId) { Button button = generateTopBarTextButton(buttonText); this.addLeftView(button, viewId, generateTopBarTextButtonLayoutParams()); return button; } /** * 在 TopBar 右边添加一个 Button,并设置文字 * * @param stringResId 按钮的文字的 resourceId * @param viewId 该按钮的id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @return 返回生成的按钮 */ public Button addRightTextButton(int stringResId, int viewId) { return addRightTextButton(getResources().getString(stringResId), viewId); } /** * 在 TopBar 右边添加一个 Button,并设置文字 * * @param buttonText 按钮的文字 * @param viewId 该按钮的 id,可在 ids.xml 中找到合适的或新增。手工指定 viewId 是为了适应自动化测试。 * @return 返回生成的按钮 */ public Button addRightTextButton(String buttonText, int viewId) { Button button = generateTopBarTextButton(buttonText); this.addRightView(button, viewId, generateTopBarTextButtonLayoutParams()); return button; } private IQMUISkinDefaultAttrProvider mTopBarTextDefaultAttrProvider; /** * 生成一个文本按钮,并设置文字 * * @param text 按钮的文字 * @return 返回生成的按钮 */ private Button generateTopBarTextButton(String text) { Button button = new Button(getContext()); if (mTopBarTextDefaultAttrProvider == null) { QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr( QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_topbar_text_btn_color_state_list); mTopBarTextDefaultAttrProvider = provider; } button.setTag(R.id.qmui_skin_default_attr_provider, mTopBarTextDefaultAttrProvider); button.setBackgroundResource(0); button.setMinWidth(0); button.setMinHeight(0); button.setMinimumWidth(0); button.setMinimumHeight(0); button.setTypeface(mTopBarTextBtnTypeface); button.setPadding(mTopBarTextBtnPaddingHor, 0, mTopBarTextBtnPaddingHor, 0); button.setTextColor(mTopBarTextBtnTextColor); button.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTopBarTextBtnTextSize); button.setGravity(Gravity.CENTER); button.setText(text); return button; } private IQMUISkinDefaultAttrProvider mTopBarImageColorTintColorProvider; /** * 生成一个图片按钮,配合 {{@link #generateTopBarImageButtonLayoutParams()} 使用 * * @param imageResourceId 图片的 resId */ private QMUIAlphaImageButton generateTopBarImageButton(int imageResourceId, boolean followTintColor) { QMUIAlphaImageButton imageButton = new QMUIAlphaImageButton(getContext()); if (followTintColor) { if (mTopBarImageColorTintColorProvider == null) { QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr( QMUISkinValueBuilder.TINT_COLOR, R.attr.qmui_skin_support_topbar_image_tint_color); mTopBarImageColorTintColorProvider = provider; } imageButton.setTag(R.id.qmui_skin_default_attr_provider, mTopBarImageColorTintColorProvider); } imageButton.setBackgroundColor(Color.TRANSPARENT); imageButton.setImageResource(imageResourceId); return imageButton; } /** * 便捷方法,在 TopBar 左边添加一个返回图标按钮 * * @return 返回按钮 */ public QMUIAlphaImageButton addLeftBackImageButton() { if(mClearLeftPaddingWhenAddLeftBackView){ QMUIViewHelper.setPaddingLeft(this, 0); } if(mLeftBackViewWidth > 0){ return addLeftImageButton(mLeftBackDrawableRes, true, R.id.qmui_topbar_item_left_back, mLeftBackViewWidth, -1); } return addLeftImageButton(mLeftBackDrawableRes, R.id.qmui_topbar_item_left_back); } /** * 移除 TopBar 左边所有的 View */ public void removeAllLeftViews() { for (View leftView : mLeftViewList) { removeView(leftView); } mLeftLastViewId = DEFAULT_VIEW_ID; mLeftViewList.clear(); } /** * 移除 TopBar 右边所有的 View */ public void removeAllRightViews() { for (View rightView : mRightViewList) { removeView(rightView); } mRightLastViewId = DEFAULT_VIEW_ID; mRightViewList.clear(); } /** * 移除 TopBar 的 centerView 和 titleView */ public void removeCenterViewAndTitleView() { if (mCenterView != null) { if (mCenterView.getParent() == this) { removeView(mCenterView); } mCenterView = null; } if (mTitleView != null) { if (mTitleView.getParent() == this) { removeView(mTitleView); } mTitleView = null; } } int getTopBarHeight() { if (mTopBarHeight == -1) { mTopBarHeight = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_topbar_height); } return mTopBarHeight; } /** * 设置 TopBar 背景的透明度 * * @param alpha 取值范围:[0, 255],255表示不透明 */ public void setBackgroundAlpha(int alpha) { this.getBackground().mutate().setAlpha(alpha); } /** * 根据当前 offset、透明度变化的初始 offset 和目标 offset,计算并设置 Topbar 的透明度 * * @param currentOffset 当前 offset * @param alphaBeginOffset 透明度开始变化的offset,即当 currentOffset == alphaBeginOffset 时,透明度为0 * @param alphaTargetOffset 透明度变化的目标offset,即当 currentOffset == alphaTargetOffset 时,透明度为1 */ public int computeAndSetBackgroundAlpha(int currentOffset, int alphaBeginOffset, int alphaTargetOffset) { double alpha = (float) (currentOffset - alphaBeginOffset) / (alphaTargetOffset - alphaBeginOffset); alpha = Math.max(0, Math.min(alpha, 1)); // from 0 to 1 int alphaInt = (int) (alpha * 255); setBackgroundAlpha(alphaInt); return alphaInt; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTitleContainerView != null) { // 计算左侧 View 的总宽度 int leftViewWidth = getPaddingLeft(); for (int leftViewIndex = 0; leftViewIndex < mLeftViewList.size(); leftViewIndex++) { View view = mLeftViewList.get(leftViewIndex); if (view.getVisibility() != GONE) { leftViewWidth += view.getMeasuredWidth(); } } // 计算右侧 View 的总宽度 int rightViewWidth = getPaddingRight(); for (int rightViewIndex = 0; rightViewIndex < mRightViewList.size(); rightViewIndex++) { View view = mRightViewList.get(rightViewIndex); if (view.getVisibility() != GONE) { rightViewWidth += view.getMeasuredWidth(); } } leftViewWidth = Math.max(mTitleMarginHorWhenNoBtnAside, leftViewWidth); rightViewWidth = Math.max(mTitleMarginHorWhenNoBtnAside, rightViewWidth); // 计算 titleContainer 的最大宽度 int titleContainerWidth; if ((mTitleGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) { // 标题水平居中,左右两侧的占位要保持一致 titleContainerWidth = MeasureSpec.getSize(widthMeasureSpec) - Math.max(leftViewWidth, rightViewWidth) * 2; } else { // 标题非水平居中,左右两侧的占位按实际计算即可 titleContainerWidth = MeasureSpec.getSize(widthMeasureSpec) - leftViewWidth - rightViewWidth; } int titleContainerWidthMeasureSpec = MeasureSpec.makeMeasureSpec(titleContainerWidth, MeasureSpec.EXACTLY); mTitleContainerView.measure(titleContainerWidthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mTitleContainerView != null) { int titleContainerViewWidth = mTitleContainerView.getMeasuredWidth(); int titleContainerViewHeight = mTitleContainerView.getMeasuredHeight(); int titleContainerViewTop = (b - t - mTitleContainerView.getMeasuredHeight()) / 2; int titleContainerViewLeft = getPaddingLeft(); if ((mTitleGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) { // 标题水平居中 titleContainerViewLeft = (r - l - mTitleContainerView.getMeasuredWidth()) / 2; } else { // 标题非水平居中 // 计算左侧 View 的总宽度 for (int leftViewIndex = 0; leftViewIndex < mLeftViewList.size(); leftViewIndex++) { View view = mLeftViewList.get(leftViewIndex); if (view.getVisibility() != GONE) { titleContainerViewLeft += view.getMeasuredWidth(); } } titleContainerViewLeft = Math.max(titleContainerViewLeft, mTitleMarginHorWhenNoBtnAside); } mTitleContainerView.layout(titleContainerViewLeft, titleContainerViewTop, titleContainerViewLeft + titleContainerViewWidth, titleContainerViewTop + titleContainerViewHeight); } } @Override public void handle(@NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme, @Nullable SimpleArrayMap attrs) { if (attrs != null) { for (int i = 0; i < attrs.size(); i++) { String key = attrs.keyAt(i); Integer attr = attrs.valueAt(i); if (attr == null) { continue; } if (getParent() instanceof QMUITopBarLayout && (QMUISkinValueBuilder.BACKGROUND.equals(key) || QMUISkinValueBuilder.BOTTOM_SEPARATOR.equals(key))) { // handled by parent continue; } manager.defaultHandleSkinAttr(this, theme, key, attr); } } } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } public void eachLeftRightView(@NonNull Action action){ for(int i = 0; i < mLeftViewList.size(); i++){ action.call(mLeftViewList.get(i), i, true); } for(int i = 0; i < mRightViewList.size(); i++){ action.call(mRightViewList.get(i), i, false); } } public interface Action { void call(View view, int index, boolean isLeftView); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUITopBarLayout.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.widget; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.FrameLayout; import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaImageButton; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; /** * 这是一个对 {@link QMUITopBar} 的代理类,需要它的原因是: * 我们用 fitSystemWindows 实现沉浸式状态栏后,需要将 {@link QMUITopBar} 的背景衍生到状态栏后面,这个时候 fitSystemWindows 是通过 * 更改 padding 实现的,而 {@link QMUITopBar} 是在高度固定的前提下做各种行为的,例如按钮的垂直居中,因此我们需要在外面包裹一层并消耗 padding * * @author cginechen * @date 2016-11-26 */ public class QMUITopBarLayout extends QMUIFrameLayout implements IQMUISkinDefaultAttrProvider { private QMUITopBar mTopBar; private SimpleArrayMap mDefaultSkinAttrs = new SimpleArrayMap<>(2); public QMUITopBarLayout(Context context) { this(context, null); } public QMUITopBarLayout(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUITopBarStyle); } public QMUITopBarLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDefaultSkinAttrs.put(QMUISkinValueBuilder.BOTTOM_SEPARATOR, R.attr.qmui_skin_support_topbar_separator_color); mDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_topbar_bg); mTopBar = new QMUITopBar(context, attrs, defStyleAttr); mTopBar.setBackground(null); mTopBar.setVisibility(View.VISIBLE); // reset these field because mTopBar will set same value with QMUITopBarLayout from attrs mTopBar.setFitsSystemWindows(false); mTopBar.setId(View.generateViewId()); mTopBar.updateBottomDivider(0, 0, 0, 0); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, mTopBar.getTopBarHeight()); addView(mTopBar, lp); QMUIWindowInsetHelper.handleWindowInsets(this, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout(), true, true); } public QMUITopBar getTopBar() { return mTopBar; } public void setCenterView(View view) { mTopBar.setCenterView(view); } public QMUIQQFaceView setTitle(int resId) { return mTopBar.setTitle(resId); } public QMUIQQFaceView setTitle(String title) { return mTopBar.setTitle(title); } public void showTitleView(boolean toShow) { mTopBar.showTitleView(toShow); } public QMUISpanTouchFixTextView setSubTitle(int resId) { return mTopBar.setSubTitle(resId); } public QMUISpanTouchFixTextView setSubTitle(CharSequence subTitle) { return mTopBar.setSubTitle(subTitle); } @Nullable public QMUIQQFaceView getTitleView(){ return mTopBar.getTitleView(); } @Nullable public QMUISpanTouchFixTextView getSubTitleView(){ return mTopBar.getSubTitleView(); } public void setTitleGravity(int gravity) { mTopBar.setTitleGravity(gravity); } public void addLeftView(View view, int viewId) { mTopBar.addLeftView(view, viewId); } public void addLeftView(View view, int viewId, RelativeLayout.LayoutParams layoutParams) { mTopBar.addLeftView(view, viewId, layoutParams); } public void addRightView(View view, int viewId) { mTopBar.addRightView(view, viewId); } public void addRightView(View view, int viewId, RelativeLayout.LayoutParams layoutParams) { mTopBar.addRightView(view, viewId, layoutParams); } public QMUIAlphaImageButton addRightImageButton(int drawableResId, int viewId) { return mTopBar.addRightImageButton(drawableResId, viewId); } public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId) { return mTopBar.addRightImageButton(drawableResId, followTintColor, viewId); } public QMUIAlphaImageButton addRightImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { return mTopBar.addRightImageButton(drawableResId, followTintColor, viewId, iconWidth, iconHeight); } public QMUIAlphaImageButton addLeftImageButton(int drawableResId, int viewId) { return mTopBar.addLeftImageButton(drawableResId, viewId); } public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId) { return mTopBar.addLeftImageButton(drawableResId, followTintColor, viewId); } public QMUIAlphaImageButton addLeftImageButton(int drawableResId, boolean followTintColor, int viewId, int iconWidth, int iconHeight) { return mTopBar.addLeftImageButton(drawableResId, followTintColor, viewId, iconWidth, iconHeight); } public Button addLeftTextButton(int stringResId, int viewId) { return mTopBar.addLeftTextButton(stringResId, viewId); } public Button addLeftTextButton(String buttonText, int viewId) { return mTopBar.addLeftTextButton(buttonText, viewId); } public Button addRightTextButton(int stringResId, int viewId) { return mTopBar.addRightTextButton(stringResId, viewId); } public Button addRightTextButton(String buttonText, int viewId) { return mTopBar.addRightTextButton(buttonText, viewId); } public QMUIAlphaImageButton addLeftBackImageButton() { return mTopBar.addLeftBackImageButton(); } public void removeAllLeftViews() { mTopBar.removeAllLeftViews(); } public void removeAllRightViews() { mTopBar.removeAllRightViews(); } public void removeCenterViewAndTitleView() { mTopBar.removeCenterViewAndTitleView(); } /** * 设置 TopBar 背景的透明度 * * @param alpha 取值范围:[0, 255],255表示不透明 */ public void setBackgroundAlpha(int alpha) { this.getBackground().mutate().setAlpha(alpha); } /** * 根据当前 offset、透明度变化的初始 offset 和目标 offset,计算并设置 Topbar 的透明度 * * @param currentOffset 当前 offset * @param alphaBeginOffset 透明度开始变化的offset,即当 currentOffset == alphaBeginOffset 时,透明度为0 * @param alphaTargetOffset 透明度变化的目标offset,即当 currentOffset == alphaTargetOffset 时,透明度为1 */ public int computeAndSetBackgroundAlpha(int currentOffset, int alphaBeginOffset, int alphaTargetOffset) { double alpha = (float) (currentOffset - alphaBeginOffset) / (alphaTargetOffset - alphaBeginOffset); alpha = Math.max(0, Math.min(alpha, 1)); // from 0 to 1 int alphaInt = (int) (alpha * 255); this.setBackgroundAlpha(alphaInt); return alphaInt; } public void setDefaultSkinAttr(String name, int attr) { mDefaultSkinAttrs.put(name, attr); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return mDefaultSkinAttrs; } public void eachLeftRightView(@NonNull QMUITopBar.Action action){ mTopBar.eachLeftRightView(action); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIVerticalTextView.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.widget; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Build; import android.text.TextPaint; import android.util.AttributeSet; import android.widget.TextView; import androidx.appcompat.widget.AppCompatTextView; /** * 在 {@link TextView} 的基础上支持文字竖排 * *

默认将文字竖排显示, 可使用 {@link #setVerticalMode(boolean)} 来开启/关闭竖排功能

*/ public class QMUIVerticalTextView extends AppCompatTextView { /** * 是否将文字显示成竖排 */ private boolean mIsVerticalMode = true; private int mLineCount; // 行数 private float[] mLineWidths; // 下标: 行号; 数组内容: 该行的宽度(由该行最宽的字符决定) private int[] mLineBreakIndex; // 下标: 行号; 数组内容: 该行最后一个字符的下标 public QMUIVerticalTextView(Context context) { super(context); init(); } public QMUIVerticalTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public QMUIVerticalTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { } @SuppressLint("DrawAllocation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mIsVerticalMode) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); float width = getPaddingLeft() + getPaddingRight(); float height = getPaddingTop() + getPaddingBottom(); char[] chars = getText().toString().toCharArray(); final Paint paint = getPaint(); final Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); final int lineMaxBottom = (heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : heightSize) - getPaddingBottom(); float currentLineHeight = getPaddingTop(); float lineMaxHeight = currentLineHeight; int lineIndex = 0; mLineCount = 0; mLineWidths = new float[chars.length + 1]; // 加1是为了处理高度不够放下一个字的情况,needBreakLine会一直为true直到最后一个字 mLineBreakIndex = new int[chars.length + 1]; // 从右向左,从上向下布局 int step = 1; for (int i = 0; i < chars.length; i += step) { int codePoint = Character.codePointAt(chars, i); step = Character.charCount(codePoint); // rotate boolean needRotate = !isCJKCharacter(codePoint); // char height float charHeight; float charWidth; if (needRotate) { charWidth = fontMetricsInt.descent - fontMetricsInt.ascent; charHeight = paint.measureText(chars, i, step); } else { charWidth = paint.measureText(chars, i, step); charHeight = fontMetricsInt.descent - fontMetricsInt.ascent; } // is need break line boolean needBreakLine = currentLineHeight + charHeight > lineMaxBottom && i > 0; // i > 0 是因为即使在第一列高度不够,也不用换下一列 if (needBreakLine) { // break line if (lineMaxHeight < currentLineHeight) { lineMaxHeight = currentLineHeight; } mLineBreakIndex[lineIndex] = i - step; width += mLineWidths[lineIndex]; lineIndex++; // reset currentLineHeight = getPaddingTop() + charHeight; } else { currentLineHeight += charHeight; if (lineMaxHeight < currentLineHeight) { lineMaxHeight = currentLineHeight; } } if (mLineWidths[lineIndex] < charWidth) { mLineWidths[lineIndex] = charWidth; } // last column width if (i + step >= chars.length) { width += mLineWidths[lineIndex]; height = lineMaxHeight + getPaddingBottom(); } } if (chars.length > 0) { mLineCount = lineIndex + 1; mLineBreakIndex[lineIndex] = chars.length - step; } // 计算 lineSpacing if (mLineCount > 1) { int lineSpacingCount = mLineCount - 1; for (int i = 0; i < lineSpacingCount; i++) { width += mLineWidths[i] * (getLineSpacingMultiplier() - 1) + getLineSpacingExtra(); } } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize); } if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(width, widthSize); } setMeasuredDimension((int) width, (int) height); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override protected void onDraw(Canvas canvas) { if (!mIsVerticalMode) { super.onDraw(canvas); } else { if (mLineCount == 0) { return; } final TextPaint paint = getPaint(); paint.setColor(getCurrentTextColor()); paint.drawableState = getDrawableState(); final Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); final char[] chars = getText().toString().toCharArray(); canvas.save(); int curLine = 0; float curLineX = getWidth() - getPaddingRight() - mLineWidths[curLine]; float curX = curLineX; float curY = getPaddingTop(); int step; for (int i = 0; i < chars.length; i+=step) { int codePoint = Character.codePointAt(chars, i); step = Character.charCount(codePoint); boolean needRotate = !isCJKCharacter(codePoint); final int saveCount = canvas.save(); if (needRotate) { canvas.rotate(90, curX, curY); } // draw float textX = curX; float textBaseline = needRotate ? curY - (mLineWidths[curLine] - (fontMetricsInt.bottom - fontMetricsInt.top)) / 2 - fontMetricsInt.descent : curY - fontMetricsInt.ascent; canvas.drawText(chars, i, step, textX, textBaseline, paint); canvas.restoreToCount(saveCount); // if break line boolean hasNextChar = i + step < chars.length; if (hasNextChar) { // boolean breakLine = needBreakLine(i, mLineCharsCount, curLine); boolean nextCharBreakLine = i + 1 > mLineBreakIndex[curLine]; if (nextCharBreakLine && curLine + 1 < mLineWidths.length) { // new line curLine++; curLineX -= (mLineWidths[curLine] * getLineSpacingMultiplier() + getLineSpacingExtra()); curX = curLineX; curY = getPaddingTop(); } else { // move to next char if (needRotate) { curY += paint.measureText(chars, i, step); } else { curY += fontMetricsInt.descent - fontMetricsInt.ascent; } } } } canvas.restore(); } } // This method is copied from moai.ik.helper.CharacterHelper.isCJKCharacter(char input) private static boolean isCJKCharacter(int input) { Character.UnicodeBlock ub = Character.UnicodeBlock.of(input); //noinspection RedundantIfStatement if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A //全角数字字符和日韩字符 || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS //韩文字符集 || ub == Character.UnicodeBlock.HANGUL_SYLLABLES || ub == Character.UnicodeBlock.HANGUL_JAMO || ub == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO //日文字符集 || ub == Character.UnicodeBlock.HIRAGANA //平假名 || ub == Character.UnicodeBlock.KATAKANA //片假名 || ub == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS ) { return true; } else { return false; } //其他的CJK标点符号,可以不做处理 //|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION //|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION } public void setVerticalMode(boolean verticalMode) { mIsVerticalMode = verticalMode; requestLayout(); } public boolean isVerticalMode() { return mIsVerticalMode; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIViewPager.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.widget; import android.content.Context; import android.database.DataSetObserver; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; /** * @author cginechen * @date 2017-09-13 */ public class QMUIViewPager extends ViewPager { private static final int DEFAULT_INFINITE_RATIO = 100; private boolean mIsSwipeable = true; private boolean mIsInMeasure = false; private boolean mEnableLoop = false; private int mInfiniteRatio = DEFAULT_INFINITE_RATIO; public QMUIViewPager(Context context) { this(context, null); } public QMUIViewPager(Context context, AttributeSet attrs) { super(context, attrs); QMUIWindowInsetHelper.overrideWithDoNotHandleWindowInsets(this); } public void setSwipeable(boolean enable) { mIsSwipeable = enable; } public int getInfiniteRatio() { return mInfiniteRatio; } public void setInfiniteRatio(int infiniteRatio) { mInfiniteRatio = infiniteRatio; } public boolean isEnableLoop() { return mEnableLoop; } public void setEnableLoop(boolean enableLoop) { if (mEnableLoop != enableLoop) { mEnableLoop = enableLoop; if (getAdapter() != null) { getAdapter().notifyDataSetChanged(); } } } @Override public void onViewAdded(View child) { super.onViewAdded(child); ViewCompat.requestApplyInsets(child); } @Override public boolean onTouchEvent(MotionEvent ev) { try { return mIsSwipeable && super.onTouchEvent(ev); } catch (IllegalArgumentException ignore) { return false; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return mIsSwipeable && super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException ignore) { return false; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mIsInMeasure = true; super.onMeasure(widthMeasureSpec, heightMeasureSpec); mIsInMeasure = false; } public boolean isInMeasure() { return mIsInMeasure; } @Override public void setAdapter(PagerAdapter adapter) { if (adapter instanceof QMUIPagerAdapter) { super.setAdapter(new WrapperPagerAdapter((QMUIPagerAdapter) adapter)); } else { super.setAdapter(adapter); } } class WrapperPagerAdapter extends PagerAdapter { private QMUIPagerAdapter mAdapter; public WrapperPagerAdapter(QMUIPagerAdapter adapter) { mAdapter = adapter; } @Override public int getCount() { int count = mAdapter.getCount(); if (mEnableLoop && count > 3) { count *= mInfiniteRatio; } return count; } @Override @NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { int realPosition = position; if (mEnableLoop && mAdapter.getCount() != 0) { realPosition = position % mAdapter.getCount(); } return mAdapter.instantiateItem(container, realPosition); } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { int realPosition = position; if (mEnableLoop && mAdapter.getCount() != 0) { realPosition = position % mAdapter.getCount(); } mAdapter.destroyItem(container, realPosition, object); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return mAdapter.isViewFromObject(view, object); } @Override public void restoreState(Parcelable bundle, ClassLoader classLoader) { mAdapter.restoreState(bundle, classLoader); } @Override public Parcelable saveState() { return mAdapter.saveState(); } @Override public void startUpdate(@NonNull ViewGroup container) { mAdapter.startUpdate(container); } @Override public void finishUpdate(@NonNull ViewGroup container) { mAdapter.finishUpdate(container); } @Override public CharSequence getPageTitle(int position) { int virtualPosition = position % mAdapter.getCount(); return mAdapter.getPageTitle(virtualPosition); } @Override public float getPageWidth(int position) { return mAdapter.getPageWidth(position); } @Override public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { mAdapter.setPrimaryItem(container, position, object); } @Override public void unregisterDataSetObserver(@NonNull DataSetObserver observer) { mAdapter.unregisterDataSetObserver(observer); } @Override public void registerDataSetObserver(@NonNull DataSetObserver observer) { mAdapter.registerDataSetObserver(observer); } @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); mAdapter.notifyDataSetChanged(); } @Override public int getItemPosition(@NonNull Object object) { return mAdapter.getItemPosition(object); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout.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.widget; import android.content.Context; import android.util.AttributeSet; import android.view.View; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; @Deprecated public class QMUIWindowInsetLayout extends QMUIFrameLayout { public QMUIWindowInsetLayout(Context context) { this(context, null); } public QMUIWindowInsetLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIWindowInsetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void onViewAdded(View child) { super.onViewAdded(child); QMUIWindowInsetHelper.handleWindowInsets(child, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWindowInsetLayout2.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.widget; import android.content.Context; import android.util.AttributeSet; import android.view.View; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; @Deprecated public class QMUIWindowInsetLayout2 extends QMUIConstraintLayout { public QMUIWindowInsetLayout2(Context context) { this(context, null); } public QMUIWindowInsetLayout2(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIWindowInsetLayout2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void setFitsSystemWindows(boolean fitSystemWindows) { // do nothing. } @Override public void onViewAdded(View view) { super.onViewAdded(view); QMUIWindowInsetHelper.handleWindowInsets(view, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWrapContentListView.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.widget; import android.content.Context; import android.util.AttributeSet; import android.widget.ListView; /** * 支持高度值为 wrap_content 的 {@link ListView},解决原生 {@link ListView} 在设置高度为 wrap_content 时高度计算错误的 bug。 */ public class QMUIWrapContentListView extends ListView { private int mMaxHeight = Integer.MAX_VALUE >> 2; public QMUIWrapContentListView(Context context){ super(context); } public QMUIWrapContentListView(Context context, int maxHeight) { super(context); mMaxHeight = maxHeight; } public QMUIWrapContentListView(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIWrapContentListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setMaxHeight(int maxHeight) { if(mMaxHeight != maxHeight){ mMaxHeight = maxHeight; requestLayout(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/QMUIWrapContentScrollView.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.widget; import android.content.Context; import android.util.AttributeSet; import android.view.ViewGroup; /** * height is wrapContent but limited by maxHeight *

* Created by cgspine on 2017/12/21. */ public class QMUIWrapContentScrollView extends QMUIObservableScrollView { private int mMaxHeight = Integer.MAX_VALUE >> 2; public QMUIWrapContentScrollView(Context context) { super(context); } public QMUIWrapContentScrollView(Context context, int maxHeight) { super(context); mMaxHeight = maxHeight; } public QMUIWrapContentScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public QMUIWrapContentScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setMaxHeight(int maxHeight) { if (mMaxHeight != maxHeight) { mMaxHeight = maxHeight; requestLayout(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ViewGroup.LayoutParams lp = getLayoutParams(); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int maxHeight = Math.min(heightSize, mMaxHeight); int expandSpec; if (lp != null && lp.height > 0 && lp.height <= mMaxHeight) { expandSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); } else { expandSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); } super.onMeasure(widthMeasureSpec, expandSpec); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBaseDialog.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.widget.dialog; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.res.TypedArray; import android.view.LayoutInflater; import android.view.Window; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDialog; import androidx.core.view.LayoutInflaterCompat; import com.qmuiteam.qmui.skin.QMUISkinLayoutInflaterFactory; import com.qmuiteam.qmui.skin.QMUISkinManager; public class QMUIBaseDialog extends AppCompatDialog { boolean cancelable = true; private boolean canceledOnTouchOutside = true; private boolean canceledOnTouchOutsideSet; private QMUISkinManager mSkinManager = null; public QMUIBaseDialog(@NonNull Context context, int themeResId) { super(context, themeResId); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); } public void setSkinManager(@Nullable QMUISkinManager skinManager) { if(mSkinManager != null){ mSkinManager.unRegister(this); } mSkinManager = skinManager; if(isShowing() && skinManager != null){ mSkinManager.register(this); } } @Override protected void onStart() { super.onStart(); if (mSkinManager != null) { mSkinManager.register(this); } } @NonNull @Override public LayoutInflater getLayoutInflater() { LayoutInflater layoutInflater = super.getLayoutInflater(); LayoutInflater.Factory2 factory2 = layoutInflater.getFactory2(); if(factory2 instanceof QMUISkinLayoutInflaterFactory){ LayoutInflaterCompat.setFactory2(layoutInflater, ((QMUISkinLayoutInflaterFactory)factory2).cloneForLayoutInflaterIfNeeded(layoutInflater)); } return layoutInflater; } @Override protected void onStop() { super.onStop(); if (mSkinManager != null) { mSkinManager.unRegister(this); } } @Override public void setCancelable(boolean cancelable) { super.setCancelable(cancelable); if (this.cancelable != cancelable) { this.cancelable = cancelable; onSetCancelable(cancelable); } } protected void onSetCancelable(boolean cancelable) { } @Override public void setCanceledOnTouchOutside(boolean cancel) { super.setCanceledOnTouchOutside(cancel); if (cancel && !cancelable) { cancelable = true; } canceledOnTouchOutside = cancel; canceledOnTouchOutsideSet = true; } protected boolean shouldWindowCloseOnTouchOutside() { if (!canceledOnTouchOutsideSet) { TypedArray a = getContext() .obtainStyledAttributes(new int[]{android.R.attr.windowCloseOnTouchOutside}); canceledOnTouchOutside = a.getBoolean(0, true); a.recycle(); canceledOnTouchOutsideSet = true; } return canceledOnTouchOutside; } @Override public void dismiss() { Context context = getContext(); if(context instanceof ContextWrapper){ context = ((ContextWrapper)context).getBaseContext(); } if(context instanceof Activity){ Activity activity = (Activity) context; if(activity.isDestroyed() || activity.isFinishing()){ return; } super.dismiss(); }else{ try{ super.dismiss(); }catch (Throwable ignore){ } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheet.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.widget.dialog; import static com.qmuiteam.qmui.layout.IQMUILayout.HIDE_RADIUS_SIDE_BOTTOM; import android.app.Dialog; import android.content.Context; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.Pair; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.LinearLayout; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * QMUIBottomSheet 在 {@link Dialog} 的基础上重新定制了 {@link #show()} 和 {@link #hide()} 时的动画效果, 使 {@link Dialog} 在界面底部升起和降下。 *

* 提供了以下两种面板样式: *

    *
  • 列表样式, 使用 {@link QMUIBottomSheet.BottomListSheetBuilder} 生成。
  • *
  • 宫格类型, 使用 {@link QMUIBottomSheet.BottomGridSheetBuilder} 生成。
  • *
*

*/ public class QMUIBottomSheet extends QMUIBaseDialog { private static final String TAG = "QMUIBottomSheet"; private QMUIBottomSheetRootLayout mRootView; private OnBottomSheetShowListener mOnBottomSheetShowListener; private QMUIBottomSheetBehavior mBehavior; private boolean mAnimateToCancel = false; private boolean mAnimateToDismiss = false; public QMUIBottomSheet(Context context) { this(context, R.style.QMUI_BottomSheet); } public QMUIBottomSheet(Context context, int style) { super(context, style); ViewGroup container = (ViewGroup) getLayoutInflater().inflate(R.layout.qmui_bottom_sheet_dialog, null); mRootView = container.findViewById(R.id.bottom_sheet); mBehavior = new QMUIBottomSheetBehavior<>(); mBehavior.setHideable(cancelable); mBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { if (mAnimateToCancel) { // cancel() invoked cancel(); } else if (mAnimateToDismiss) { // dismiss() invoked but it it not triggered by cancel() dismiss(); } else { // drag to cancel cancel(); } } } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }); mBehavior.setPeekHeight(0); mBehavior.setAllowDrag(false); mBehavior.setSkipCollapsed(true); CoordinatorLayout.LayoutParams rootViewLp = (CoordinatorLayout.LayoutParams) mRootView.getLayoutParams(); rootViewLp.setBehavior(mBehavior); // We treat the CoordinatorLayout as outside the dialog though it is technically inside container.findViewById(R.id.touch_outside) .setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { if(mBehavior.getState() == BottomSheetBehavior.STATE_SETTLING){ return; } if (cancelable && isShowing() && shouldWindowCloseOnTouchOutside()) { cancel(); } } }); mRootView.setOnTouchListener( new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { // Consume the event and prevent it from falling through return true; } }); super.setContentView(container, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } @Override protected void onSetCancelable(boolean cancelable) { super.onSetCancelable(cancelable); mBehavior.setHideable(cancelable); } public void setFitNav(boolean fitNav) { if(fitNav){ mRootView.setFitsSystemWindows(true); QMUIWindowInsetHelper.handleWindowInsets(mRootView, WindowInsetsCompat.Type.navigationBars(), getInsetHandler(), true, true, false); }else{ mRootView.setFitsSystemWindows(false); QMUIWindowInsetHelper.setOnApplyWindowInsetsListener(mRootView, null, true); } mRootView.requestApplyInsets(); } protected QMUIWindowInsetHelper.InsetHandler getInsetHandler(){ return QMUIWindowInsetHelper.consumeInsetWithPaddingHandler; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Window window = getWindow(); if (window != null) { window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } ViewCompat.requestApplyInsets(mRootView); } @Override protected void onStart() { super.onStart(); if (mBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } } @Override public void cancel() { if (mBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { mAnimateToCancel = false; super.cancel(); } else { mAnimateToCancel = true; mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); } } @Override public void dismiss() { if (mBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { mAnimateToDismiss = false; super.dismiss(); } else { mAnimateToDismiss = true; mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); } } public void setOnBottomSheetShowListener(OnBottomSheetShowListener onBottomSheetShowListener) { mOnBottomSheetShowListener = onBottomSheetShowListener; } public void setRadius(int radius) { mRootView.setRadius(radius, HIDE_RADIUS_SIDE_BOTTOM); } public QMUIBottomSheetRootLayout getRootView() { return mRootView; } public QMUIBottomSheetBehavior getBehavior() { return mBehavior; } @Override public void show() { super.show(); if (mOnBottomSheetShowListener != null) { mOnBottomSheetShowListener.onShow(); } if (mBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED) { setToExpandWhenShow(); } mAnimateToCancel = false; mAnimateToDismiss = false; } protected void setToExpandWhenShow(){ mRootView.postOnAnimation(new Runnable() { @Override public void run() { mBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); } }); } public interface OnBottomSheetShowListener { void onShow(); } @Override public void setContentView(View view) { throw new IllegalStateException( "Use addContentView(View, ConstraintLayout.LayoutParams) for replacement"); } @Override public void setContentView(int layoutResId) { throw new IllegalStateException( "Use addContentView(int) for replacement"); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { throw new IllegalStateException( "Use addContentView(View, QMUIPriorityLinearLayout.LayoutParams) for replacement"); } @Override public void addContentView(View view, ViewGroup.LayoutParams params) { throw new IllegalStateException( "Use addContentView(View, QMUIPriorityLinearLayout.LayoutParams) for replacement"); } public void addContentView(View view, QMUIPriorityLinearLayout.LayoutParams layoutParams) { mRootView.addView(view, layoutParams); } public void addContentView(View view) { QMUIPriorityLinearLayout.LayoutParams lp = new QMUIPriorityLinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.setPriority(QMUIPriorityLinearLayout.LayoutParams.PRIORITY_DISPOSABLE); mRootView.addView(view, lp); } public void addContentView(int layoutResId) { LayoutInflater.from(mRootView.getContext()).inflate(layoutResId, mRootView, true); } /** * 生成列表类型的 {@link QMUIBottomSheet} 对话框。 */ public static class BottomListSheetBuilder extends QMUIBottomSheetBaseBuilder { private List mItems; private List mContentHeaderViews; private List mContentFooterViews; private boolean mNeedRightMark; //是否需要rightMark,标识当前项 private int mCheckedIndex; private boolean mGravityCenter = false; private OnSheetItemClickListener mOnSheetItemClickListener; public BottomListSheetBuilder(Context context) { this(context, false); } /** * @param needRightMark 是否需要在被选中的 Item 右侧显示一个勾(使用 {@link #setCheckedIndex(int)} 设置选中的 Item) */ public BottomListSheetBuilder(Context context, boolean needRightMark) { super(context); mItems = new ArrayList<>(); mNeedRightMark = needRightMark; } /** * 设置要被选中的 Item 的下标。 *

* 注意:仅当 {@link #mNeedRightMark} 为 true 时才有效。 */ public BottomListSheetBuilder setCheckedIndex(int checkedIndex) { mCheckedIndex = checkedIndex; return this; } public BottomListSheetBuilder setNeedRightMark(boolean needRightMark) { mNeedRightMark = needRightMark; return this; } public BottomListSheetBuilder setGravityCenter(boolean gravityCenter) { mGravityCenter = gravityCenter; return this; } public BottomListSheetBuilder setOnSheetItemClickListener( OnSheetItemClickListener onSheetItemClickListener) { mOnSheetItemClickListener = onSheetItemClickListener; return this; } public BottomListSheetBuilder addItem(QMUIBottomSheetListItemModel itemModel) { mItems.add(itemModel); return this; } /** * @param textAndTag Item 的文字内容,同时会把内容设置为 tag。 */ public BottomListSheetBuilder addItem(String textAndTag) { mItems.add(new QMUIBottomSheetListItemModel(textAndTag, textAndTag)); return this; } /** * @param image icon Item 的 icon。 * @param textAndTag Item 的文字内容,同时会把内容设置为 tag。 */ public BottomListSheetBuilder addItem(Drawable image, String textAndTag) { mItems.add(new QMUIBottomSheetListItemModel(textAndTag, textAndTag).image(image)); return this; } /** * @param text Item 的文字内容。 * @param tag item 的 tag。 */ public BottomListSheetBuilder addItem(String text, String tag) { mItems.add(new QMUIBottomSheetListItemModel(text, tag)); return this; } /** * @param imageRes Item 的图标 Resource。 * @param text Item 的文字内容。 * @param tag Item 的 tag。 */ public BottomListSheetBuilder addItem(int imageRes, String text, String tag) { mItems.add(new QMUIBottomSheetListItemModel(text, tag).image(imageRes)); return this; } /** * @param imageRes Item 的图标 Resource。 * @param text Item 的文字内容。 * @param tag Item 的 tag。 * @param hasRedPoint 是否显示红点。 */ public BottomListSheetBuilder addItem(int imageRes, String text, String tag, boolean hasRedPoint) { mItems.add(new QMUIBottomSheetListItemModel(text, tag).image(imageRes).redPoint(hasRedPoint)); return this; } /** * @param imageRes Item 的图标 Resource。 * @param text Item 的文字内容。 * @param tag Item 的 tag。 * @param hasRedPoint 是否显示红点。 * @param disabled 是否显示禁用态。 */ public BottomListSheetBuilder addItem( int imageRes, CharSequence text, String tag, boolean hasRedPoint, boolean disabled) { mItems.add(new QMUIBottomSheetListItemModel(text, tag) .image(imageRes).redPoint(hasRedPoint).disabled(disabled)); return this; } @Deprecated public BottomListSheetBuilder addHeaderView(@NonNull View view) { return addContentHeaderView(view); } public BottomListSheetBuilder addContentHeaderView(@NonNull View view) { if (mContentHeaderViews == null) { mContentHeaderViews = new ArrayList<>(); } mContentHeaderViews.add(view); return this; } public BottomListSheetBuilder addContentFooterView(@NonNull View view) { if (mContentFooterViews == null) { mContentFooterViews = new ArrayList<>(); } mContentFooterViews.add(view); return this; } @Nullable @Override protected View onCreateContentView(@NonNull final QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { RecyclerView recyclerView = new RecyclerView(context); recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); QMUIBottomSheetListAdapter adapter = new QMUIBottomSheetListAdapter( mNeedRightMark, mGravityCenter); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(context) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); recyclerView.addItemDecoration(new QMUIBottomSheetListItemDecoration(context)); LinearLayout headerView = null; if (mContentHeaderViews != null && mContentHeaderViews.size() > 0) { headerView = new LinearLayout(context); headerView.setOrientation(LinearLayout.VERTICAL); for (View view : mContentHeaderViews) { if (view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); } headerView.addView(view, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } } LinearLayout footerView = null; if (mContentFooterViews != null && mContentFooterViews.size() > 0) { footerView = new LinearLayout(context); footerView.setOrientation(LinearLayout.VERTICAL); for (View view : mContentFooterViews) { if (view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); } footerView.addView(view, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } } adapter.setData(headerView, footerView, mItems); adapter.setOnItemClickListener(new QMUIBottomSheetListAdapter.OnItemClickListener() { @Override public void onClick(QMUIBottomSheetListAdapter.VH vh, int dataPos, QMUIBottomSheetListItemModel model) { if (mOnSheetItemClickListener != null) { mOnSheetItemClickListener.onClick(bottomSheet, vh.itemView, dataPos, model.tag); } } }); adapter.setCheckedIndex(mCheckedIndex); recyclerView.scrollToPosition(mCheckedIndex + (headerView == null ? 0 : 1)); return recyclerView; } public interface OnSheetItemClickListener { void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag); } } /** * 生成宫格类型的 {@link QMUIBottomSheet} 对话框。 */ public static class BottomGridSheetBuilder extends QMUIBottomSheetBaseBuilder implements View.OnClickListener { public static final int FIRST_LINE = 0; public static final int SECOND_LINE = 1; public static final ItemViewFactory DEFAULT_ITEM_VIEW_FACTORY = new DefaultItemViewFactory(); public interface ItemViewFactory { QMUIBottomSheetGridItemView create(QMUIBottomSheet bottomSheet, QMUIBottomSheetGridItemModel model); } public static class DefaultItemViewFactory implements ItemViewFactory { @Override public QMUIBottomSheetGridItemView create(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetGridItemModel model) { QMUIBottomSheetGridItemView itemView = new QMUIBottomSheetGridItemView(bottomSheet.getContext()); itemView.render(model); return itemView; } } private ArrayList mFirstLineItems; private ArrayList mSecondLineItems; private ItemViewFactory mItemViewFactory = DEFAULT_ITEM_VIEW_FACTORY; private OnSheetItemClickListener mOnSheetItemClickListener; private QMUIBottomSheetGridLineLayout.ItemWidthCalculator mItemWidthCalculator = null; private int mLineGravity = Gravity.CENTER_VERTICAL; public BottomGridSheetBuilder(Context context) { super(context); mFirstLineItems = new ArrayList<>(); mSecondLineItems = new ArrayList<>(); } public BottomGridSheetBuilder setLineGravity(int gravity){ mLineGravity = gravity; return this; } public BottomGridSheetBuilder addItem(@NonNull QMUIBottomSheetGridItemModel model, @Style int style) { switch (style) { case FIRST_LINE: mFirstLineItems.add(model); break; case SECOND_LINE: mSecondLineItems.add(model); break; } return this; } public BottomGridSheetBuilder addItem(int imageRes, CharSequence textAndTag, @Style int style) { return addItem(imageRes, textAndTag, textAndTag, style, 0); } public BottomGridSheetBuilder addItem(int imageRes, CharSequence text, Object tag, @Style int style) { return addItem(imageRes, text, tag, style, 0); } public BottomGridSheetBuilder addItem(int imageRes, CharSequence text, Object tag, @Style int style, int subscriptRes) { return addItem(imageRes, text, tag, style, subscriptRes, null); } public BottomGridSheetBuilder addItem(int imageRes, CharSequence text, Object tag, @Style int style, int subscriptRes, Typeface typeface) { return addItem(new QMUIBottomSheetGridItemModel(text, tag) .image(imageRes) .subscript(subscriptRes) .typeface(typeface), style); } public void setItemViewFactory(ItemViewFactory itemViewFactory) { mItemViewFactory = itemViewFactory; } public BottomGridSheetBuilder setOnSheetItemClickListener(OnSheetItemClickListener onSheetItemClickListener) { mOnSheetItemClickListener = onSheetItemClickListener; return this; } public BottomGridSheetBuilder setItemWidthCalculator(QMUIBottomSheetGridLineLayout.ItemWidthCalculator itemWidthCalculator) { mItemWidthCalculator = itemWidthCalculator; return this; } @Override public void onClick(View v) { if (mOnSheetItemClickListener != null) { mOnSheetItemClickListener.onClick(mDialog, v); } } @Nullable @Override protected View onCreateContentView(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { if (mFirstLineItems.isEmpty() && mSecondLineItems.isEmpty()) { return null; } List> firstLines = null; List> secondLines = null; int wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT; if (!mFirstLineItems.isEmpty()) { firstLines = new ArrayList<>(); for (QMUIBottomSheetGridItemModel model : mFirstLineItems) { QMUIBottomSheetGridItemView itemView = mItemViewFactory.create(bottomSheet, model); itemView.setOnClickListener(this); firstLines.add(new Pair( itemView, new LinearLayout.LayoutParams(wrapContent, wrapContent))); } } if (!mSecondLineItems.isEmpty()) { secondLines = new ArrayList<>(); for (QMUIBottomSheetGridItemModel model : mSecondLineItems) { QMUIBottomSheetGridItemView itemView = mItemViewFactory.create(bottomSheet, model); itemView.setOnClickListener(this); secondLines.add(new Pair( itemView, new LinearLayout.LayoutParams(wrapContent, wrapContent))); } } return new QMUIBottomSheetGridLineLayout(mDialog, mItemWidthCalculator, mLineGravity, firstLines, secondLines); } public interface OnSheetItemClickListener { void onClick(QMUIBottomSheet dialog, View itemView); } @Retention(RetentionPolicy.SOURCE) @IntDef({FIRST_LINE, SECOND_LINE}) public @interface Style { } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBaseBuilder.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.widget.dialog; import android.content.Context; import android.content.DialogInterface; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIButton; import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; public abstract class QMUIBottomSheetBaseBuilder { private Context mContext; protected QMUIBottomSheet mDialog; private CharSequence mTitle; private boolean mAddCancelBtn; private String mCancelText; private DialogInterface.OnDismissListener mOnBottomDialogDismissListener; private int mRadius = -1; private boolean mAllowDrag = false; private QMUISkinManager mSkinManager; private QMUIBottomSheetBehavior.DownDragDecisionMaker mDownDragDecisionMaker = null; private boolean fitNav = true; public QMUIBottomSheetBaseBuilder(Context context) { mContext = context; } @SuppressWarnings("unchecked") public T setTitle(CharSequence title) { mTitle = title; return (T) this; } protected boolean hasTitle() { return mTitle != null && mTitle.length() != 0; } @SuppressWarnings("unchecked") public T setAllowDrag(boolean allowDrag) { mAllowDrag = allowDrag; return (T) this; } @SuppressWarnings("unchecked") public T setSkinManager(@Nullable QMUISkinManager skinManager) { mSkinManager = skinManager; return (T) this; } @SuppressWarnings("unchecked") public T setFitNav(boolean fitNav) { this.fitNav = fitNav; return (T) this; } @SuppressWarnings("unchecked") public T setDownDragDecisionMaker(QMUIBottomSheetBehavior.DownDragDecisionMaker downDragDecisionMaker) { mDownDragDecisionMaker = downDragDecisionMaker; return (T) this; } @SuppressWarnings("unchecked") public T setAddCancelBtn(boolean addCancelBtn) { mAddCancelBtn = addCancelBtn; return (T) this; } @SuppressWarnings("unchecked") public T setCancelText(String cancelText) { mCancelText = cancelText; return (T) this; } @SuppressWarnings("unchecked") public T setRadius(int radius) { mRadius = radius; return (T) this; } @SuppressWarnings("unchecked") public T setOnBottomDialogDismissListener(DialogInterface.OnDismissListener listener) { mOnBottomDialogDismissListener = listener; return (T) this; } public QMUIBottomSheet build() { return build(R.style.QMUI_BottomSheet); } public QMUIBottomSheet build(int style) { mDialog = new QMUIBottomSheet(mContext, style); Context dialogContext = mDialog.getContext(); QMUIBottomSheetRootLayout rootLayout = mDialog.getRootView(); rootLayout.removeAllViews(); View titleView = onCreateTitleView(mDialog, rootLayout, dialogContext); if (titleView != null) { mDialog.addContentView(titleView); } onAddCustomViewBetweenTitleAndContent(mDialog, rootLayout, dialogContext); View contentView = onCreateContentView(mDialog, rootLayout, dialogContext); if (contentView != null) { QMUIPriorityLinearLayout.LayoutParams lp = new QMUIPriorityLinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.setPriority(QMUIPriorityLinearLayout.LayoutParams.PRIORITY_DISPOSABLE); mDialog.addContentView(contentView, lp); } onAddCustomViewAfterContent(mDialog, rootLayout, dialogContext); if (mAddCancelBtn) { mDialog.addContentView(onCreateCancelBtn(mDialog, rootLayout, dialogContext), new QMUIPriorityLinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, QMUIResHelper.getAttrDimen(dialogContext, R.attr.qmui_bottom_sheet_cancel_btn_height))); } if (mOnBottomDialogDismissListener != null) { mDialog.setOnDismissListener(mOnBottomDialogDismissListener); } if (mRadius != -1) { mDialog.setRadius(mRadius); } mDialog.setSkinManager(mSkinManager); mDialog.setFitNav(fitNav); QMUIBottomSheetBehavior behavior = mDialog.getBehavior(); behavior.setAllowDrag(mAllowDrag); behavior.setDownDragDecisionMaker(mDownDragDecisionMaker); return mDialog; } @Nullable protected View onCreateTitleView(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { if (hasTitle()) { QMUISpanTouchFixTextView tv = new QMUISpanTouchFixTextView(context); tv.setId(R.id.qmui_bottom_sheet_title); tv.setText(mTitle); tv.onlyShowBottomDivider(0, 0, 1, QMUIResHelper.getAttrColor(context, R.attr.qmui_skin_support_bottom_sheet_separator_color)); QMUIResHelper.assignTextViewWithAttr(tv, R.attr.qmui_bottom_sheet_title_style); QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.textColor(R.attr.qmui_skin_support_bottom_sheet_title_text_color); valueBuilder.bottomSeparator(R.attr.qmui_skin_support_bottom_sheet_separator_color); QMUISkinHelper.setSkinValue(tv, valueBuilder); valueBuilder.release(); return tv; } return null; } protected void onAddCustomViewBetweenTitleAndContent(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { } @Nullable protected abstract View onCreateContentView(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context); protected void onAddCustomViewAfterContent(@NonNull QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { } @NonNull protected View onCreateCancelBtn(@NonNull final QMUIBottomSheet bottomSheet, @NonNull QMUIBottomSheetRootLayout rootLayout, @NonNull Context context) { QMUIButton button = new QMUIButton(context); button.setId(R.id.qmui_bottom_sheet_cancel); if (mCancelText == null || mCancelText.isEmpty()) { mCancelText = context.getString(R.string.qmui_cancel); } button.setPadding(0, 0,0, 0); button.setBackground(QMUIResHelper.getAttrDrawable( context, R.attr.qmui_skin_support_bottom_sheet_cancel_bg)); button.setText(mCancelText); QMUIResHelper.assignTextViewWithAttr(button, R.attr.qmui_bottom_sheet_cancel_style); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { bottomSheet.cancel(); } }); button.onlyShowTopDivider(0, 0, 1, QMUIResHelper.getAttrColor( context, R.attr.qmui_skin_support_bottom_sheet_separator_color)); QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.textColor(R.attr.qmui_skin_support_bottom_sheet_cancel_text_color); valueBuilder.topSeparator(R.attr.qmui_skin_support_bottom_sheet_separator_color); valueBuilder.background(R.attr.qmui_skin_support_bottom_sheet_cancel_bg); QMUISkinHelper.setSkinValue(button, valueBuilder); valueBuilder.release(); return button; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetBehavior.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.widget.dialog; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; public class QMUIBottomSheetBehavior extends BottomSheetBehavior { private boolean mAllowDrag = true; private boolean mMotionEventCanDrag = true; private DownDragDecisionMaker mDownDragDecisionMaker; public void setAllowDrag(boolean allowDrag) { mAllowDrag = allowDrag; } public void setDownDragDecisionMaker(DownDragDecisionMaker downDragDecisionMaker) { mDownDragDecisionMaker = downDragDecisionMaker; } @Override public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { if(!mAllowDrag){ return false; } if(event.getAction() == MotionEvent.ACTION_DOWN){ mMotionEventCanDrag = mDownDragDecisionMaker == null || mDownDragDecisionMaker.canDrag(parent, child, event); } if(!mMotionEventCanDrag){ return false; } return super.onTouchEvent(parent, child, event); } @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { if(!mAllowDrag){ return false; } if(event.getAction() == MotionEvent.ACTION_DOWN){ mMotionEventCanDrag = mDownDragDecisionMaker == null || mDownDragDecisionMaker.canDrag(parent, child, event); } if(!mMotionEventCanDrag){ return false; } return super.onInterceptTouchEvent(parent, child, event); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { if(!mAllowDrag){ return false; } return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type); } public interface DownDragDecisionMaker { boolean canDrag(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent event); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridItemModel.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.widget.dialog; import android.graphics.Typeface; import android.graphics.drawable.Drawable; public class QMUIBottomSheetGridItemModel { Drawable image = null; int imageRes = 0; int imageSkinTintColorAttr = 0; int imageSkinSrcAttr = 0; int textSkinColorAttr = 0; CharSequence text; Object tag = ""; Drawable subscript = null; int subscriptRes = 0; int subscriptSkinTintColorAttr = 0; int subscriptSkinSrcAttr = 0; Typeface typeface; public QMUIBottomSheetGridItemModel(CharSequence text, Object tag) { this.text = text; this.tag = tag; } public QMUIBottomSheetGridItemModel image(Drawable image) { this.image = image; return this; } public QMUIBottomSheetGridItemModel image(int imageRes) { this.imageRes = imageRes; return this; } public QMUIBottomSheetGridItemModel subscript(Drawable image) { this.subscript = image; return this; } public QMUIBottomSheetGridItemModel subscript(int imageRes) { this.subscriptRes = imageRes; return this; } public QMUIBottomSheetGridItemModel skinTextColorAttr(int attr) { this.textSkinColorAttr = attr; return this; } public QMUIBottomSheetGridItemModel skinImageTintColorAttr(int attr) { this.imageSkinTintColorAttr = attr; return this; } public QMUIBottomSheetGridItemModel skinImageSrcAttr(int attr) { this.imageSkinSrcAttr = attr; return this; } public QMUIBottomSheetGridItemModel skinSubscriptTintColorAttr(int attr) { this.subscriptSkinTintColorAttr = attr; return this; } public QMUIBottomSheetGridItemModel skinSubscriptSrcAttr(int attr) { this.subscriptSkinSrcAttr = attr; return this; } public QMUIBottomSheetGridItemModel typeface(Typeface typeface) { this.typeface = typeface; return this; } public CharSequence getText() { return text; } public Drawable getImage() { return image; } public Drawable getSubscript() { return subscript; } public int getImageRes() { return imageRes; } public int getImageSkinSrcAttr() { return imageSkinSrcAttr; } public int getImageSkinTintColorAttr() { return imageSkinTintColorAttr; } public int getSubscriptRes() { return subscriptRes; } public int getSubscriptSkinSrcAttr() { return subscriptSkinSrcAttr; } public int getSubscriptSkinTintColorAttr() { return subscriptSkinTintColorAttr; } public int getTextSkinColorAttr() { return textSkinColorAttr; } public Object getTag() { return tag; } public Typeface getTypeface() { return typeface; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridItemView.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.widget.dialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageView; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.QMUISkinSimpleDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; public class QMUIBottomSheetGridItemView extends QMUIConstraintLayout { protected AppCompatImageView mIconIv; protected AppCompatImageView mSubscriptIv; protected TextView mTitleTv; protected Object mModelTag; public QMUIBottomSheetGridItemView(Context context) { this(context, null); } public QMUIBottomSheetGridItemView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIBottomSheetGridItemView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setChangeAlphaWhenPress(true); int paddingTop = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_grid_item_padding_top); int paddingBottom = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_grid_item_padding_bottom); setPadding(0, paddingTop, 0, paddingBottom); mIconIv = onCreateIconView(context); mIconIv.setId(View.generateViewId()); mIconIv.setScaleType(ImageView.ScaleType.CENTER_INSIDE); int iconSize = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_grid_item_icon_size); LayoutParams iconLp = new LayoutParams(iconSize, iconSize); iconLp.leftToLeft = LayoutParams.PARENT_ID; iconLp.rightToRight = LayoutParams.PARENT_ID; iconLp.topToTop = LayoutParams.PARENT_ID; addView(mIconIv, iconLp); mTitleTv = onCreateTitleView(context); mTitleTv.setId(View.generateViewId()); QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr(QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_bottom_sheet_grid_item_text_color); QMUIResHelper.assignTextViewWithAttr(mTitleTv, R.attr.qmui_bottom_sheet_grid_item_text_style); QMUISkinHelper.setSkinDefaultProvider(mTitleTv, provider); LayoutParams titleLp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); titleLp.leftToLeft = LayoutParams.PARENT_ID; titleLp.rightToRight = LayoutParams.PARENT_ID; titleLp.topToBottom = mIconIv.getId(); titleLp.topMargin = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_grid_item_text_margin_top); addView(mTitleTv, titleLp); } protected AppCompatImageView onCreateIconView(Context context) { return new AppCompatImageView(context); } protected TextView onCreateTitleView(Context context) { return new QMUISpanTouchFixTextView(context); } public void render(@NonNull QMUIBottomSheetGridItemModel model) { mModelTag = model.tag; setTag(model.tag); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); renderIcon(model, builder); builder.clear(); renderTitle(model, builder); builder.clear(); renderSubScript(model, builder); builder.release(); } public Object getModelTag() { return mModelTag; } protected void renderIcon(@NonNull QMUIBottomSheetGridItemModel model, @NonNull QMUISkinValueBuilder builder) { if (model.imageSkinSrcAttr != 0) { builder.src(model.imageSkinSrcAttr); QMUISkinHelper.setSkinValue(mIconIv, builder); Drawable drawable = QMUISkinHelper.getSkinDrawable(mIconIv, model.imageSkinSrcAttr); mIconIv.setImageDrawable(drawable); } else { Drawable drawable = model.image; if (drawable == null && model.imageRes != 0) { drawable = ContextCompat.getDrawable(getContext(), model.imageRes); } if (drawable != null) { drawable.mutate(); } mIconIv.setImageDrawable(drawable); if (model.imageSkinTintColorAttr != 0) { builder.tintColor(model.imageSkinTintColorAttr); QMUISkinHelper.setSkinValue(mIconIv, builder); } else { QMUISkinHelper.setSkinValue(mIconIv, ""); } } } protected void renderTitle(@NonNull QMUIBottomSheetGridItemModel model, @NonNull QMUISkinValueBuilder builder) { mTitleTv.setText(model.text); if (model.textSkinColorAttr != 0) { builder.textColor(model.textSkinColorAttr); } QMUISkinHelper.setSkinValue(mTitleTv, builder); if (model.typeface != null) { mTitleTv.setTypeface(model.typeface); } } protected void renderSubScript(@NonNull QMUIBottomSheetGridItemModel model, @NonNull QMUISkinValueBuilder builder) { if (model.subscriptRes != 0 || model.subscript != null || model.subscriptSkinSrcAttr != 0) { if (mSubscriptIv == null) { mSubscriptIv = new AppCompatImageView(getContext()); mSubscriptIv.setScaleType(ImageView.ScaleType.CENTER_INSIDE); LayoutParams lp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.rightToRight = mIconIv.getId(); lp.topToTop = mIconIv.getId(); addView(mSubscriptIv, lp); } mSubscriptIv.setVisibility(View.VISIBLE); if (model.subscriptSkinSrcAttr != 0) { builder.src(model.subscriptSkinSrcAttr); QMUISkinHelper.setSkinValue(mSubscriptIv, builder); Drawable drawable = QMUISkinHelper.getSkinDrawable(mSubscriptIv, model.subscriptSkinSrcAttr); mIconIv.setImageDrawable(drawable); } else { Drawable drawable = model.subscript; if (drawable == null && model.subscriptRes != 0) { drawable = ContextCompat.getDrawable(getContext(), model.subscriptRes); } if (drawable != null) { drawable.mutate(); } mSubscriptIv.setImageDrawable(drawable); if (model.subscriptSkinTintColorAttr != 0) { builder.tintColor(model.subscriptSkinTintColorAttr); QMUISkinHelper.setSkinValue(mSubscriptIv, builder); } else { QMUISkinHelper.setSkinValue(mSubscriptIv, ""); } } } else if (mSubscriptIv != null) { mSubscriptIv.setVisibility(View.GONE); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetGridLineLayout.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.widget.dialog; import android.content.Context; import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIResHelper; import java.util.List; public class QMUIBottomSheetGridLineLayout extends LinearLayout { private static ItemWidthCalculator DEFAULT_CALCULATOR = new ItemWidthCalculator() { @Override public int calculate(Context context, int width, int miniWidth, int itemCount, int paddingLeft, int paddingRight) { final int parentSpacing = width - paddingLeft - paddingRight; int itemWidth = miniWidth; // there is no more space for the last one item. then stretch the item width if (itemCount >= 3 && parentSpacing - itemCount * itemWidth > 0 && parentSpacing - itemCount * itemWidth < itemWidth) { int count = parentSpacing / itemWidth; itemWidth = parentSpacing / count; } // if there are more items. then show half of the first that is exceeded // to tell user that there are more. if (itemWidth * itemCount > parentSpacing) { int count = (width - paddingLeft) / itemWidth; itemWidth = (int) ((width - paddingLeft) / (count + .5f)); } return itemWidth; } }; private int maxItemCountInLines; private int miniItemWidth = -1; private List> mFirstLineViews; private List> mSecondLineViews; private int linePaddingHor; private int itemWidth; private final ItemWidthCalculator mItemWidthCalculator; private final int mLineGravity; public QMUIBottomSheetGridLineLayout(QMUIBottomSheet bottomSheet, @Nullable ItemWidthCalculator widthCalculator, int lineGravity, List> firstLineViews, List> secondLineViews) { super(bottomSheet.getContext()); setOrientation(VERTICAL); setGravity(Gravity.TOP); mLineGravity = lineGravity; mItemWidthCalculator = widthCalculator == null ? DEFAULT_CALCULATOR : widthCalculator; int paddingTop = QMUIResHelper.getAttrDimen( bottomSheet.getContext(), R.attr.qmui_bottom_sheet_grid_padding_top); int paddingBottom = QMUIResHelper.getAttrDimen( bottomSheet.getContext(), R.attr.qmui_bottom_sheet_grid_padding_bottom); setPadding(0, paddingTop, 0, paddingBottom); mFirstLineViews = firstLineViews; mSecondLineViews = secondLineViews; maxItemCountInLines = Math.max( firstLineViews != null ? firstLineViews.size() : 0, secondLineViews != null ? secondLineViews.size() : 0); linePaddingHor = QMUIResHelper.getAttrDimen( bottomSheet.getContext(), R.attr.qmui_bottom_sheet_padding_hor); boolean hasFirstLine = false; if (firstLineViews != null && !firstLineViews.isEmpty()) { hasFirstLine = true; HorizontalScrollView firstLine = createHorScroller(bottomSheet, firstLineViews); addView(firstLine, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } if (secondLineViews != null && !secondLineViews.isEmpty()) { HorizontalScrollView secondLine = createHorScroller(bottomSheet, secondLineViews); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); if (hasFirstLine) { lp.topMargin = QMUIResHelper.getAttrDimen( bottomSheet.getContext(), R.attr.qmui_bottom_sheet_grid_line_vertical_space); } addView(secondLine, lp); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measureWidth = MeasureSpec.getSize(widthMeasureSpec); itemWidth = calculateItemWidth( measureWidth, maxItemCountInLines, linePaddingHor, linePaddingHor); if (mFirstLineViews != null) { for (Pair pair : mFirstLineViews) { if (pair.second.width != itemWidth) { pair.second.width = itemWidth; } } } if (mSecondLineViews != null) { for (Pair pair : mSecondLineViews) { if (pair.second.width != itemWidth) { pair.second.width = itemWidth; } } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } protected HorizontalScrollView createHorScroller( QMUIBottomSheet bottomSheet, List> itemViews) { Context context = bottomSheet.getContext(); HorizontalScrollView scroller = new HorizontalScrollView(context); scroller.setHorizontalScrollBarEnabled(false); scroller.setClipToPadding(true); LinearLayout linear = new LinearLayout(context); linear.setOrientation(LinearLayout.HORIZONTAL); linear.setGravity(mLineGravity); linear.setPadding(linePaddingHor, 0, linePaddingHor, 0); scroller.addView(linear, new HorizontalScrollView.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); for (int i = 0; i < itemViews.size(); i++) { Pair pair = itemViews.get(i); linear.addView(pair.first, pair.second); } return scroller; } @Override protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { super.measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec); } private int calculateItemWidth(int width, int calculateCount, int paddingLeft, int paddingRight) { if (miniItemWidth == -1) { miniItemWidth = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_bottom_sheet_grid_item_mini_width); } return mItemWidthCalculator.calculate(getContext(), width, miniItemWidth, calculateCount, paddingLeft, paddingRight); } public interface ItemWidthCalculator { int calculate(Context context, int width, int miniWidth, int itemCount, int paddingLeft, int paddingRight); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListAdapter.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.widget.dialog; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class QMUIBottomSheetListAdapter extends RecyclerView.Adapter { public static final int ITEM_TYPE_HEADER = 1; public static final int ITEM_TYPE_FOOTER = 2; public static final int ITEM_TYPE_NORMAL = 3; @Nullable private View mHeaderView; @Nullable private View mFooterView; private List mData = new ArrayList<>(); private final boolean mNeedMark; private final boolean mGravityCenter; private int mCheckedIndex = -1; private OnItemClickListener mOnItemClickListener; public QMUIBottomSheetListAdapter(boolean needMark, boolean gravityCenter){ mNeedMark = needMark; mGravityCenter = gravityCenter; } public void setCheckedIndex(int checkedIndex) { mCheckedIndex = checkedIndex; notifyDataSetChanged(); } public void setOnItemClickListener(OnItemClickListener onItemClickListener) { mOnItemClickListener = onItemClickListener; } public void setData(@Nullable View headerView, @Nullable View footerView, List data) { mHeaderView = headerView; mFooterView = footerView; mData.clear(); if (data != null) { mData.addAll(data); } notifyDataSetChanged(); } @Override public int getItemViewType(int position) { if(mHeaderView != null){ if(position == 0){ return ITEM_TYPE_HEADER; } } if(position == getItemCount() - 1){ if(mFooterView != null){ return ITEM_TYPE_FOOTER; } } return ITEM_TYPE_NORMAL; } @NonNull @Override public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if(viewType == ITEM_TYPE_HEADER){ return new VH(mHeaderView); }else if(viewType == ITEM_TYPE_FOOTER){ return new VH(mFooterView); } final VH vh = new VH(new QMUIBottomSheetListItemView( parent.getContext(), mNeedMark, mGravityCenter)); vh.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(mOnItemClickListener != null){ int adapterPosition = vh.getAdapterPosition(); int dataPos = mHeaderView != null ? adapterPosition - 1 : adapterPosition; mOnItemClickListener.onClick(vh, dataPos, mData.get(dataPos)); } } }); return vh; } @Override public void onBindViewHolder(@NonNull VH holder, int position) { if(holder.getItemViewType() != ITEM_TYPE_NORMAL){ return; } if(mHeaderView != null){ position--; } QMUIBottomSheetListItemModel itemModel = mData.get(position); QMUIBottomSheetListItemView itemView = (QMUIBottomSheetListItemView) holder.itemView; itemView.render(itemModel, position == mCheckedIndex); } @Override public int getItemCount() { return mData.size() + (mHeaderView != null ? 1 : 0) + (mFooterView != null ? 1 : 0); } public static class VH extends RecyclerView.ViewHolder { public VH(@NonNull View itemView) { super(itemView); } } public interface OnItemClickListener { void onClick(VH vh, int dataPos, QMUIBottomSheetListItemModel model); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemDecoration.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.widget.dialog; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.IQMUISkinHandlerDecoration; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; public class QMUIBottomSheetListItemDecoration extends RecyclerView.ItemDecoration implements IQMUISkinHandlerDecoration { private final Paint mSeparatorPaint; private final int mSeparatorAttr; public QMUIBottomSheetListItemDecoration(Context context) { mSeparatorPaint = new Paint(); mSeparatorPaint.setStrokeWidth( QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_list_item_separator_height)); mSeparatorPaint.setStyle(Paint.Style.STROKE); mSeparatorAttr = R.attr.qmui_skin_support_bottom_sheet_separator_color; if (mSeparatorAttr != 0) { mSeparatorPaint.setColor(QMUIResHelper.getAttrColor(context, mSeparatorAttr)); } } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); RecyclerView.Adapter adapter = parent.getAdapter(); RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (adapter == null || layoutManager == null || mSeparatorAttr == 0) { return; } for (int i = 0; i < parent.getChildCount(); i++) { View view = parent.getChildAt(i); int position = parent.getChildAdapterPosition(view); if (view instanceof QMUIBottomSheetListItemView) { if (position > 0 && adapter.getItemViewType(position - 1) != QMUIBottomSheetListAdapter.ITEM_TYPE_NORMAL) { int top = layoutManager.getDecoratedTop(view); c.drawLine(0, top, parent.getWidth(), top, mSeparatorPaint); } if (position + 1 < adapter.getItemCount() && adapter.getItemViewType(position + 1) == QMUIBottomSheetListAdapter.ITEM_TYPE_NORMAL) { int bottom = layoutManager.getDecoratedBottom(view); c.drawLine(0, bottom, parent.getWidth(), bottom, mSeparatorPaint); } } } } @Override public void handle(@NotNull RecyclerView recyclerView, @NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme) { if (mSeparatorAttr != 0) { mSeparatorPaint.setColor(QMUIResHelper.getAttrColor(theme, mSeparatorAttr)); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemModel.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.widget.dialog; import android.graphics.Typeface; import android.graphics.drawable.Drawable; public class QMUIBottomSheetListItemModel { Drawable image = null; int imageRes = 0; int imageSkinTintColorAttr = 0; int imageSkinSrcAttr = 0; int textSkinColorAttr = 0; CharSequence text; String tag = ""; boolean hasRedPoint = false; boolean isDisabled = false; Typeface typeface; public QMUIBottomSheetListItemModel(CharSequence text, String tag) { this.text = text; this.tag = tag; } public QMUIBottomSheetListItemModel image(Drawable image) { this.image = image; return this; } public QMUIBottomSheetListItemModel image(int imageRes) { this.imageRes = imageRes; return this; } public QMUIBottomSheetListItemModel skinTextColorAttr(int attr) { this.textSkinColorAttr = attr; return this; } public QMUIBottomSheetListItemModel skinImageTintColorAttr(int attr) { this.imageSkinTintColorAttr = attr; return this; } public QMUIBottomSheetListItemModel skinImageSrcAttr(int attr) { this.imageSkinSrcAttr = attr; return this; } public QMUIBottomSheetListItemModel redPoint(boolean hasRedPoint) { this.hasRedPoint = hasRedPoint; return this; } public QMUIBottomSheetListItemModel disabled(boolean isDisabled) { this.isDisabled = isDisabled; return this; } public QMUIBottomSheetListItemModel typeface(Typeface typeface){ this.typeface = typeface; return this; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetListItemView.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.widget.dialog; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.QMUISkinSimpleDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; public class QMUIBottomSheetListItemView extends QMUIConstraintLayout { private AppCompatImageView mIconView; private QMUISpanTouchFixTextView mTextView; private QMUIFrameLayout mRedPointView; private AppCompatImageView mMarkView = null; private int mItemHeight; public QMUIBottomSheetListItemView(Context context, boolean markStyle, boolean gravityCenter) { super(context); setBackground(QMUIResHelper.getAttrDrawable( context, R.attr.qmui_skin_support_bottom_sheet_list_item_bg)); int paddingHor = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_padding_hor); setPadding(paddingHor, 0, paddingHor, 0); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.background(R.attr.qmui_skin_support_bottom_sheet_list_item_bg); QMUISkinHelper.setSkinValue(this, builder); builder.clear(); mIconView = new AppCompatImageView(context); mIconView.setId(View.generateViewId()); mIconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); mTextView = new QMUISpanTouchFixTextView(context); mTextView.setId(View.generateViewId()); QMUISkinSimpleDefaultAttrProvider provider = new QMUISkinSimpleDefaultAttrProvider(); provider.setDefaultSkinAttr(QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_bottom_sheet_list_item_text_color); QMUIResHelper.assignTextViewWithAttr(mTextView, R.attr.qmui_bottom_sheet_list_item_text_style); QMUISkinHelper.setSkinDefaultProvider(mTextView, provider); mRedPointView = new QMUIFrameLayout(context); mRedPointView.setId(View.generateViewId()); mRedPointView.setBackgroundColor(QMUIResHelper.getAttrColor( context, R.attr.qmui_skin_support_bottom_sheet_list_red_point_color)); builder.background(R.attr.qmui_skin_support_bottom_sheet_list_red_point_color); QMUISkinHelper.setSkinValue(mRedPointView, builder); builder.clear(); if (markStyle) { mMarkView = new AppCompatImageView(context); mMarkView.setId(View.generateViewId()); mMarkView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); mMarkView.setImageDrawable(QMUIResHelper.getAttrDrawable( context, R.attr.qmui_skin_support_bottom_sheet_list_mark)); builder.src(R.attr.qmui_skin_support_bottom_sheet_list_mark); QMUISkinHelper.setSkinValue(mMarkView, builder); } builder.release(); int iconSize = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_list_item_icon_size); LayoutParams lp = new ConstraintLayout.LayoutParams(iconSize, iconSize); lp.leftToLeft = LayoutParams.PARENT_ID; lp.topToTop = LayoutParams.PARENT_ID; lp.rightToLeft = mTextView.getId(); lp.bottomToBottom = LayoutParams.PARENT_ID; lp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; lp.horizontalBias = gravityCenter ? 0.5f : 0f; addView(mIconView, lp); lp = new ConstraintLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToRight = mIconView.getId(); lp.rightToLeft = mRedPointView.getId(); lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; lp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; lp.horizontalBias = gravityCenter ? 0.5f : 0f; lp.leftMargin = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_list_item_icon_margin_right); lp.goneLeftMargin = 0; addView(mTextView, lp); int redPointSize = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_list_item_red_point_size); lp = new ConstraintLayout.LayoutParams(redPointSize, redPointSize); lp.leftToRight = mTextView.getId(); if (markStyle) { lp.rightToLeft = mMarkView.getId(); lp.rightMargin = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_list_item_mark_margin_left); } else { lp.rightToRight = LayoutParams.PARENT_ID; } lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; lp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; lp.horizontalBias = gravityCenter ? 0.5f : 0f; lp.leftMargin = QMUIResHelper.getAttrDimen( context, R.attr.qmui_bottom_sheet_list_item_tip_point_margin_left); addView(mRedPointView, lp); if (markStyle) { lp = new ConstraintLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.rightToRight = LayoutParams.PARENT_ID; lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; addView(mMarkView, lp); } mItemHeight = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_list_item_height); } public void render(@NonNull QMUIBottomSheetListItemModel itemModel, boolean isChecked) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (itemModel.imageSkinSrcAttr != 0) { builder.src(itemModel.imageSkinSrcAttr); QMUISkinHelper.setSkinValue(mIconView, builder); mIconView.setImageDrawable( QMUISkinHelper.getSkinDrawable(this, itemModel.imageSkinSrcAttr)); mIconView.setVisibility(View.VISIBLE); } else { Drawable drawable = itemModel.image; if (drawable == null && itemModel.imageRes != 0) { drawable = ContextCompat.getDrawable(getContext(), itemModel.imageRes); } if (drawable != null) { drawable.mutate(); mIconView.setImageDrawable(drawable); if (itemModel.imageSkinTintColorAttr != 0) { builder.tintColor(itemModel.imageSkinTintColorAttr); QMUISkinHelper.setSkinValue(mIconView, builder); } else { QMUISkinHelper.setSkinValue(mIconView, ""); } } else { mIconView.setVisibility(View.GONE); } } builder.clear(); mTextView.setText(itemModel.text); if (itemModel.typeface != null) { mTextView.setTypeface(itemModel.typeface); } if (itemModel.textSkinColorAttr != 0) { builder.textColor(itemModel.textSkinColorAttr); QMUISkinHelper.setSkinValue(mTextView, builder); ColorStateList color = QMUISkinHelper.getSkinColorStateList(mTextView, itemModel.textSkinColorAttr); if (color != null) { mTextView.setTextColor(color); } } else { QMUISkinHelper.setSkinValue(mTextView, ""); } mRedPointView.setVisibility(itemModel.hasRedPoint ? View.VISIBLE : View.GONE); if (mMarkView != null) { mMarkView.setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY)); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIBottomSheetRootLayout.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.widget.dialog; import android.content.Context; import android.util.AttributeSet; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; public class QMUIBottomSheetRootLayout extends QMUIPriorityLinearLayout { private final int mUsePercentMinHeight; private final float mHeightPercent; private final int mMaxWidth; public QMUIBottomSheetRootLayout(Context context) { this(context, null); } public QMUIBottomSheetRootLayout(Context context, AttributeSet attrs) { super(context, attrs); setOrientation(VERTICAL); setBackground(QMUIResHelper.getAttrDrawable(context, R.attr.qmui_skin_support_bottom_sheet_bg)); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.background(R.attr.qmui_skin_support_bottom_sheet_bg); QMUISkinHelper.setSkinValue(this, builder); builder.release(); int radius = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_radius); if (radius > 0) { setRadius(radius, HIDE_RADIUS_SIDE_BOTTOM); } mUsePercentMinHeight = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_use_percent_min_height); mHeightPercent = QMUIResHelper.getAttrFloatValue(context, R.attr.qmui_bottom_sheet_height_percent); mMaxWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_bottom_sheet_max_width); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthSize > mMaxWidth) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode); } int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightSize >= mUsePercentMinHeight) { heightMeasureSpec = MeasureSpec.makeMeasureSpec( (int) (heightSize * mHeightPercent), MeasureSpec.AT_MOST); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialog.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.widget.dialog; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.text.InputType; import android.text.TextWatcher; import android.text.method.TransformationMethod; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.layout.QMUILinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import java.util.ArrayList; import java.util.BitSet; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatImageView; import androidx.constraintlayout.widget.ConstraintLayout; /** * QMUIDialog 对话框一般由 {@link QMUIDialogBuilder} 及其子类创建, 不同的 Builder 可以创建不同类型的对话框, * 例如消息类型的对话框、菜单项对话框等等。 * * @author cginechen * @date 2015-10-20 * @see QMUIDialogBuilder */ public class QMUIDialog extends QMUIBaseDialog { private Context mBaseContext; public QMUIDialog(Context context) { this(context, R.style.QMUI_Dialog); } public QMUIDialog(Context context, int styleRes) { super(context, styleRes); mBaseContext = context; init(); } private void init() { setCancelable(true); setCanceledOnTouchOutside(true); } public void showWithImmersiveCheck(Activity activity) { // http://stackoverflow.com/questions/22794049/how-to-maintain-the-immersive-mode-in-dialogs Window window = getWindow(); if (window == null) { return; } Window activityWindow = activity.getWindow(); int activitySystemUi = activityWindow.getDecorView().getSystemUiVisibility(); if ((activitySystemUi & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN || (activitySystemUi & View.SYSTEM_UI_FLAG_FULLSCREEN) == View.SYSTEM_UI_FLAG_FULLSCREEN) { window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); window.getDecorView().setSystemUiVisibility( activity.getWindow().getDecorView().getSystemUiVisibility()); super.show(); window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); } else { super.show(); } } public void showWithImmersiveCheck() { if (!(mBaseContext instanceof Activity)) { super.show(); return; } Activity activity = (Activity) mBaseContext; showWithImmersiveCheck(activity); } /** * 消息类型的对话框 Builder。通过它可以生成一个带标题、文本消息、按钮的对话框。 */ public static class MessageDialogBuilder extends QMUIDialogBuilder { protected CharSequence mMessage; public MessageDialogBuilder(Context context) { super(context); } /** * 设置对话框的消息文本 */ public MessageDialogBuilder setMessage(CharSequence message) { this.mMessage = message; return this; } /** * 设置对话框的消息文本 */ public MessageDialogBuilder setMessage(int resId) { return setMessage(getBaseContext().getResources().getString(resId)); } @Nullable @Override protected View onCreateContent(@NonNull QMUIDialog dialog, @NonNull QMUIDialogView parent, @NonNull Context context) { if (mMessage != null && mMessage.length() != 0) { QMUISpanTouchFixTextView tv = new QMUISpanTouchFixTextView(context); assignMessageTvWithAttr(tv, hasTitle(), R.attr.qmui_dialog_message_content_style); tv.setText(mMessage); tv.setMovementMethodDefault(); QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.textColor(R.attr.qmui_skin_support_dialog_message_text_color); QMUISkinHelper.setSkinValue(tv, valueBuilder); QMUISkinValueBuilder.release(valueBuilder); return wrapWithScroll(tv); } return null; } @Nullable @Override protected View onCreateTitle(@NonNull QMUIDialog dialog, @NonNull QMUIDialogView parent, @NonNull Context context) { View tv = super.onCreateTitle(dialog, parent, context); if (tv != null && (mMessage == null || mMessage.length() == 0)) { TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogTitleTvCustomDef, R.attr.qmui_dialog_title_style, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogTitleTvCustomDef_qmui_paddingBottomWhenNotContent) { tv.setPadding( tv.getPaddingLeft(), tv.getPaddingTop(), tv.getPaddingRight(), a.getDimensionPixelSize(attr, tv.getPaddingBottom()) ); } } a.recycle(); } return tv; } public static void assignMessageTvWithAttr(TextView messageTv, boolean hasTitle, int defAttr) { QMUIResHelper.assignTextViewWithAttr(messageTv, defAttr); if (!hasTitle) { TypedArray a = messageTv.getContext().obtainStyledAttributes(null, R.styleable.QMUIDialogMessageTvCustomDef, defAttr, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMessageTvCustomDef_qmui_paddingTopWhenNotTitle) { messageTv.setPadding( messageTv.getPaddingLeft(), a.getDimensionPixelSize(attr, messageTv.getPaddingTop()), messageTv.getPaddingRight(), messageTv.getPaddingBottom() ); } } a.recycle(); } } } /** * 带 CheckBox 的消息确认框 Builder */ public static class CheckBoxMessageDialogBuilder extends QMUIDialogBuilder { protected String mMessage; private boolean mIsChecked = false; private QMUISpanTouchFixTextView mTextView; public CheckBoxMessageDialogBuilder(Context context) { super(context); } /** * 设置对话框的消息文本 */ public CheckBoxMessageDialogBuilder setMessage(String message) { this.mMessage = message; return this; } /** * 设置对话框的消息文本 */ public CheckBoxMessageDialogBuilder setMessage(int resid) { return setMessage(getBaseContext().getResources().getString(resid)); } /** * CheckBox 是否处于勾选状态 */ public boolean isChecked() { return mIsChecked; } /** * 设置 CheckBox 的勾选状态 */ public CheckBoxMessageDialogBuilder setChecked(boolean checked) { if (mIsChecked != checked) { mIsChecked = checked; if (mTextView != null) { mTextView.setSelected(checked); } } return this; } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { if (mMessage != null && mMessage.length() != 0) { mTextView = new QMUISpanTouchFixTextView(context); mTextView.setMovementMethodDefault(); MessageDialogBuilder.assignMessageTvWithAttr(mTextView, hasTitle(), R.attr.qmui_dialog_message_content_style); mTextView.setText(mMessage); Drawable drawable = QMUISkinHelper.getSkinDrawable(mTextView, R.attr.qmui_skin_support_s_dialog_check_drawable); if (drawable != null) { drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); mTextView.setCompoundDrawables(drawable, null, null, null); } QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.textColor(R.attr.qmui_skin_support_dialog_message_text_color); valueBuilder.textCompoundLeftSrc(R.attr.qmui_skin_support_s_dialog_check_drawable); QMUISkinHelper.setSkinValue(mTextView, valueBuilder); QMUISkinValueBuilder.release(valueBuilder); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setChecked(!mIsChecked); } }); mTextView.setSelected(mIsChecked); return wrapWithScroll(mTextView); } return null; } @Deprecated public QMUISpanTouchFixTextView getTextView() { return mTextView; } } /** * 带输入框的对话框 Builder */ public static class EditTextDialogBuilder extends QMUIDialogBuilder { protected String mPlaceholder; protected TransformationMethod mTransformationMethod; protected EditText mEditText; protected AppCompatImageView mRightImageView; private int mInputType = InputType.TYPE_CLASS_TEXT; private CharSequence mDefaultText = null; private TextWatcher mTextWatcher; public EditTextDialogBuilder(Context context) { super(context); } /** * 设置输入框的 placeholder */ public EditTextDialogBuilder setPlaceholder(String placeholder) { this.mPlaceholder = placeholder; return this; } /** * 设置输入框的 placeholder */ public EditTextDialogBuilder setPlaceholder(int resId) { return setPlaceholder(getBaseContext().getResources().getString(resId)); } public EditTextDialogBuilder setDefaultText(CharSequence defaultText) { mDefaultText = defaultText; return this; } /** * 设置 EditText 的 transformationMethod */ public EditTextDialogBuilder setTransformationMethod(TransformationMethod transformationMethod) { mTransformationMethod = transformationMethod; return this; } /** * 设置 EditText 的 inputType */ public EditTextDialogBuilder setInputType(int inputType) { mInputType = inputType; return this; } public EditTextDialogBuilder setTextWatcher(TextWatcher textWatcher) { mTextWatcher = textWatcher; return this; } @Override protected ConstraintLayout.LayoutParams onCreateContentLayoutParams(Context context) { ConstraintLayout.LayoutParams lp = super.onCreateContentLayoutParams(context); int marginHor = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_padding_horizontal); lp.leftMargin = marginHor; lp.rightMargin = marginHor; lp.topMargin = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_edit_margin_top); lp.bottomMargin = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_edit_margin_bottom); return lp; } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { QMUIConstraintLayout boxLayout = new QMUIConstraintLayout(context); boxLayout.onlyShowBottomDivider(0, 0, QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_edit_bottom_line_height), QMUIResHelper.getAttrColor(context, R.attr.qmui_skin_support_dialog_edit_bottom_line_color)); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.bottomSeparator(R.attr.qmui_skin_support_dialog_edit_bottom_line_color); QMUISkinHelper.setSkinValue(boxLayout, builder); mEditText = new AppCompatEditText(context); mEditText.setBackgroundResource(0); MessageDialogBuilder.assignMessageTvWithAttr(mEditText, hasTitle(), R.attr.qmui_dialog_edit_content_style); mEditText.setFocusable(true); mEditText.setFocusableInTouchMode(true); mEditText.setImeOptions(EditorInfo.IME_ACTION_GO); mEditText.setId(R.id.qmui_dialog_edit_input); if (!QMUILangHelper.isNullOrEmpty(mDefaultText)) { mEditText.setText(mDefaultText); } if (mTextWatcher != null) { mEditText.addTextChangedListener(mTextWatcher); } builder.clear(); builder.textColor(R.attr.qmui_skin_support_dialog_edit_text_color); builder.hintColor(R.attr.qmui_skin_support_dialog_edit_text_hint_color); QMUISkinHelper.setSkinValue(mEditText, builder); QMUISkinValueBuilder.release(builder); mRightImageView = new AppCompatImageView(context); mRightImageView.setId(R.id.qmui_dialog_edit_right_icon); mRightImageView.setVisibility(View.GONE); configRightImageView(mRightImageView, mEditText); if (mTransformationMethod != null) { mEditText.setTransformationMethod(mTransformationMethod); } else { mEditText.setInputType(mInputType); } if (mPlaceholder != null) { mEditText.setHint(mPlaceholder); } boxLayout.addView(mEditText, createEditTextLayoutParams(context)); boxLayout.addView(mRightImageView, createRightIconLayoutParams(context)); return boxLayout; } protected void configRightImageView(AppCompatImageView imageView, EditText editText) { } protected ConstraintLayout.LayoutParams createEditTextLayoutParams(Context context) { ConstraintLayout.LayoutParams editLp = new ConstraintLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT); editLp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; editLp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; editLp.rightToLeft = R.id.qmui_dialog_edit_right_icon; editLp.rightToRight = QMUIDisplayHelper.dp2px(context, 5); editLp.goneRightMargin = 0; return editLp; } protected ConstraintLayout.LayoutParams createRightIconLayoutParams(Context context) { ConstraintLayout.LayoutParams rightIconLp = new ConstraintLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); rightIconLp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; rightIconLp.bottomToBottom = R.id.qmui_dialog_edit_input; return rightIconLp; } @Override protected void onAfterCreate(QMUIDialog dialog, QMUIDialogRootLayout rootLayout, Context context) { super.onAfterCreate(dialog, rootLayout, context); final InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); dialog.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { inputMethodManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0); } }); mEditText.postDelayed(new Runnable() { @Override public void run() { mEditText.requestFocus(); inputMethodManager.showSoftInput(mEditText, 0); } }, 300); } /** * 注意该方法只在调用 {@link #create()} 或 {@link #create(int)} 或 {@link #show()} 生成 Dialog 之后 * 才能返回对应的 EditText,在此之前将返回 null */ @Deprecated public EditText getEditText() { return mEditText; } public ImageView getRightImageView() { return mRightImageView; } } public static class MenuBaseDialogBuilder extends QMUIDialogBuilder { protected ArrayList mMenuItemViewsFactoryList; protected ArrayList mMenuItemViews = new ArrayList<>(); public MenuBaseDialogBuilder(Context context) { super(context); mMenuItemViewsFactoryList = new ArrayList<>(); } public void clear() { mMenuItemViewsFactoryList.clear(); } @SuppressWarnings("unchecked") @Deprecated public T addItem(final QMUIDialogMenuItemView itemView, final OnClickListener listener) { itemView.setMenuIndex(mMenuItemViewsFactoryList.size()); itemView.setListener(new QMUIDialogMenuItemView.MenuItemViewListener() { @Override public void onClick(int index) { onItemClick(index); if (listener != null) { listener.onClick(mDialog, index); } } }); mMenuItemViewsFactoryList.add(new ItemViewFactory() { @Override public QMUIDialogMenuItemView createItemView(Context context) { return itemView; } }); return (T) this; } public T addItem(final ItemViewFactory itemViewFactory, final OnClickListener listener) { mMenuItemViewsFactoryList.add(new ItemViewFactory() { @Override public QMUIDialogMenuItemView createItemView(Context context) { QMUIDialogMenuItemView itemView = itemViewFactory.createItemView(context); itemView.setMenuIndex(mMenuItemViewsFactoryList.indexOf(this)); itemView.setListener(new QMUIDialogMenuItemView.MenuItemViewListener() { @Override public void onClick(int index) { onItemClick(index); if (listener != null) { listener.onClick(mDialog, index); } } }); return itemView; } }); return (T) this; } protected void onItemClick(int index) { } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { LinearLayout layout = new QMUILinearLayout(context); layout.setOrientation(LinearLayout.VERTICAL); TypedArray a = context.obtainStyledAttributes( null, R.styleable.QMUIDialogMenuContainerStyleDef, R.attr.qmui_dialog_menu_container_style, 0); int count = a.getIndexCount(); int paddingTop = 0, paddingBottom = 0, paddingVerWhenSingle = 0, paddingTopWhenTitle = 0, paddingBottomWhenAction = 0, itemHeight = -1; for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_android_paddingTop) { paddingTop = a.getDimensionPixelSize(attr, paddingTop); } else if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_android_paddingBottom) { paddingBottom = a.getDimensionPixelSize(attr, paddingBottom); } else if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_qmui_dialog_menu_container_single_padding_vertical) { paddingVerWhenSingle = a.getDimensionPixelSize(attr, paddingVerWhenSingle); } else if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_qmui_dialog_menu_container_padding_top_when_title_exist) { paddingTopWhenTitle = a.getDimensionPixelSize(attr, paddingTopWhenTitle); } else if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_qmui_dialog_menu_container_padding_bottom_when_action_exist) { paddingBottomWhenAction = a.getDimensionPixelSize(attr, paddingBottomWhenAction); } else if (attr == R.styleable.QMUIDialogMenuContainerStyleDef_qmui_dialog_menu_item_height) { itemHeight = a.getDimensionPixelSize(attr, itemHeight); } } a.recycle(); if (mMenuItemViewsFactoryList.size() == 1) { paddingBottom = paddingTop = paddingVerWhenSingle; } if (hasTitle()) { paddingTop = paddingTopWhenTitle; } if (mActions.size() > 0) { paddingBottom = paddingBottomWhenAction; } layout.setPadding(0, paddingTop, 0, paddingBottom); LinearLayout.LayoutParams itemLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, itemHeight); itemLp.gravity = Gravity.CENTER_VERTICAL; mMenuItemViews.clear(); for (ItemViewFactory factory : mMenuItemViewsFactoryList) { QMUIDialogMenuItemView itemView = factory.createItemView(context); layout.addView(itemView, itemLp); mMenuItemViews.add(itemView); } return wrapWithScroll(layout); } public interface ItemViewFactory { QMUIDialogMenuItemView createItemView(Context context); } } /** * 菜单类型的对话框 Builder */ public static class MenuDialogBuilder extends MenuBaseDialogBuilder { public MenuDialogBuilder(Context context) { super(context); } /** * 添加多个菜单项 * * @param items 所有菜单项的文字 * @param listener 菜单项的点击事件 */ public MenuDialogBuilder addItems(CharSequence[] items, OnClickListener listener) { for (final CharSequence item : items) { addItem(item, listener); } return this; } /** * 添加单个菜单项 * * @param item 菜单项的文字 * @param listener 菜单项的点击事件 */ public MenuDialogBuilder addItem(final CharSequence item, OnClickListener listener) { addItem(new ItemViewFactory() { @Override public QMUIDialogMenuItemView createItemView(Context context) { return new QMUIDialogMenuItemView.TextItemView(context, item); } }, listener); return this; } } /** * 单选类型的对话框 Builder */ public static class CheckableDialogBuilder extends MenuBaseDialogBuilder { /** * 当前被选中的菜单项的下标, 负数表示没选中任何项 */ private int mCheckedIndex = -1; public CheckableDialogBuilder(Context context) { super(context); } /** * 获取当前选中的菜单项的下标 * * @return 负数表示没选中任何项 */ public int getCheckedIndex() { return mCheckedIndex; } /** * 设置选中的菜单项的下班 */ public CheckableDialogBuilder setCheckedIndex(int checkedIndex) { mCheckedIndex = checkedIndex; return this; } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { View result = super.onCreateContent(dialog, parent, context); if (mCheckedIndex > -1 && mCheckedIndex < mMenuItemViews.size()) { mMenuItemViews.get(mCheckedIndex).setChecked(true); } return result; } @Override protected void onItemClick(int index) { for (int i = 0; i < mMenuItemViews.size(); i++) { QMUIDialogMenuItemView itemView = mMenuItemViews.get(i); if (i == index) { itemView.setChecked(true); mCheckedIndex = index; } else { itemView.setChecked(false); } } } /** * 添加菜单项 * * @param items 所有菜单项的文字 * @param listener 菜单项的点击事件,可以在点击事件里调用 {@link #setCheckedIndex(int)} 来设置选中某些菜单项 */ public CheckableDialogBuilder addItems(CharSequence[] items, OnClickListener listener) { for (final CharSequence item : items) { addItem(new ItemViewFactory() { @Override public QMUIDialogMenuItemView createItemView(Context context) { return new QMUIDialogMenuItemView.MarkItemView(context, item); } }, listener); } return this; } } /** * 多选类型的对话框 Builder */ public static class MultiCheckableDialogBuilder extends MenuBaseDialogBuilder { /** * 该 int 的每一位标识菜单的每一项是否被选中 (1为选中,0位不选中) */ private BitSet mCheckedItems = new BitSet(); public MultiCheckableDialogBuilder(Context context) { super(context); } /** * 设置被选中的菜单项的下标 * * @param checkedItems 注意: 该 int 参数的每一位标识菜单项的每一项是否被选中 *

如 20 表示选中下标为 1、3 的菜单项, 因为 (2<<1) + (2<<3) = 20

*/ public MultiCheckableDialogBuilder setCheckedItems(BitSet checkedItems) { mCheckedItems.clear(); mCheckedItems.or(checkedItems); return this; } /** * 设置被选中的菜单项的下标 * * @param checkedIndexes 被选中的菜单项的下标组成的数组,如 [1,3] 表示选中下标为 1、3 的菜单项 */ public MultiCheckableDialogBuilder setCheckedItems(int[] checkedIndexes) { mCheckedItems.clear(); if (checkedIndexes != null && checkedIndexes.length > 0) { for (int checkedIndex : checkedIndexes) { mCheckedItems.set(checkedIndex); } } return this; } /** * 添加菜单项 * * @param items 所有菜单项的文字 * @param listener 菜单项的点击事件,可以在点击事件里调用 {@link #setCheckedItems(int[])}} 来设置选中某些菜单项 */ public MultiCheckableDialogBuilder addItems(CharSequence[] items, OnClickListener listener) { for (final CharSequence item : items) { addItem(new ItemViewFactory() { @Override public QMUIDialogMenuItemView createItemView(Context context) { return new QMUIDialogMenuItemView.CheckItemView(context, true, item); } }, listener); } return this; } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { View result = super.onCreateContent(dialog, parent, context); for (int i = 0; i < mMenuItemViews.size(); i++) { QMUIDialogMenuItemView itemView = mMenuItemViews.get(i); itemView.setChecked(mCheckedItems.get(i)); } return result; } @Override protected void onItemClick(int index) { QMUIDialogMenuItemView itemView = mMenuItemViews.get(index); itemView.setChecked(!itemView.isChecked()); mCheckedItems.set(index, itemView.isChecked()); } /** * @return 被选中的菜单项的下标 注意: 如果选中的是1,3项(以0开始),因为 (2<<1) + (2<<3) = 20 */ public BitSet getCheckedItemRecord() { return (BitSet) mCheckedItems.clone(); } /** * @return 被选中的菜单项的下标数组。如果选中的是1,3项(以0开始),则返回[1,3] */ public int[] getCheckedItemIndexes() { ArrayList array = new ArrayList<>(); int length = mMenuItemViews.size(); for (int i = 0; i < length; i++) { QMUIDialogMenuItemView itemView = mMenuItemViews.get(i); if (itemView.isChecked()) { array.add(itemView.getMenuIndex()); } } int[] output = new int[array.size()]; for (int i = 0; i < array.size(); i++) { output[i] = array.get(i); } return output; } protected boolean existCheckedItem() { return !mCheckedItems.isEmpty(); } } /** * 自定义对话框内容区域的 Builder */ public static class CustomDialogBuilder extends QMUIDialogBuilder { private int mLayoutId; public CustomDialogBuilder(Context context) { super(context); } /** * 设置内容区域的 layoutResId */ public CustomDialogBuilder setLayout(@LayoutRes int layoutResId) { mLayoutId = layoutResId; return this; } @Nullable @Override protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { return LayoutInflater.from(context).inflate(mLayoutId, parent, false); } } /** * 随键盘升降自动调整 Dialog 高度的 Builder */ public static abstract class AutoResizeDialogBuilder extends QMUIDialogBuilder { protected ScrollView mScrollView; public AutoResizeDialogBuilder(Context context) { super(context); setCheckKeyboardOverlay(true); } @Nullable @Override protected View onCreateContent(@NonNull QMUIDialog dialog,@NonNull QMUIDialogView parent, @NonNull Context context) { mScrollView = wrapWithScroll(onBuildContent(dialog, context)); return mScrollView; } public abstract View onBuildContent(@NonNull QMUIDialog dialog, @NonNull Context context); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogAction.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.widget.dialog; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.util.TypedValue; import android.view.View; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIButton; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUISpanHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; /** * @author cginechen * @date 2015-10-20 */ public class QMUIDialogAction { @IntDef({ACTION_PROP_NEGATIVE, ACTION_PROP_NEUTRAL, ACTION_PROP_POSITIVE}) @Retention(RetentionPolicy.SOURCE) public @interface Prop { } //用于标记positive/negative/neutral public static final int ACTION_PROP_POSITIVE = 0; public static final int ACTION_PROP_NEUTRAL = 1; public static final int ACTION_PROP_NEGATIVE = 2; private CharSequence mStr; private int mIconRes = 0; private int mActionProp = ACTION_PROP_NEUTRAL; private int mSkinTextColorAttr = 0; private int mSkinBackgroundAttr = 0; private int mSkinIconTintColorAttr = 0; private int mSkinSeparatorColorAttr = R.attr.qmui_skin_support_dialog_action_divider_color; private ActionListener mOnClickListener; private QMUIButton mButton; private boolean mIsEnabled = true; public QMUIDialogAction(Context context, int strRes) { this(context.getResources().getString(strRes)); } public QMUIDialogAction(CharSequence str) { this(str, null); } public QMUIDialogAction(Context context, int strRes, @Nullable ActionListener onClickListener) { this(context.getResources().getString(strRes), onClickListener); } public QMUIDialogAction(CharSequence str, @Nullable ActionListener onClickListener) { mStr = str; mOnClickListener = onClickListener; } public QMUIDialogAction prop(@Prop int actionProp) { mActionProp = actionProp; return this; } public QMUIDialogAction iconRes(@Prop int iconRes) { mIconRes = iconRes; return this; } public QMUIDialogAction onClick(ActionListener onClickListener) { mOnClickListener = onClickListener; return this; } public QMUIDialogAction skinTextColorAttr(int skinTextColorAttr) { mSkinTextColorAttr = skinTextColorAttr; return this; } public QMUIDialogAction skinBackgroundAttr(int skinBackgroundAttr) { mSkinBackgroundAttr = skinBackgroundAttr; return this; } public QMUIDialogAction skinIconTintColorAttr(int skinIconTintColorAttr) { mSkinIconTintColorAttr = skinIconTintColorAttr; return this; } /** * inner usage * @param skinSeparatorColorAttr * @return */ QMUIDialogAction skinSeparatorColorAttr(int skinSeparatorColorAttr){ mSkinSeparatorColorAttr = skinSeparatorColorAttr; return this; } public QMUIDialogAction setEnabled(boolean enabled) { mIsEnabled = enabled; if (mButton != null) { mButton.setEnabled(enabled); } return this; } public QMUIButton buildActionView(final QMUIDialog dialog, final int index) { mButton = generateActionButton(dialog.getContext(), mStr, mIconRes, mSkinBackgroundAttr, mSkinTextColorAttr, mSkinIconTintColorAttr); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mOnClickListener != null && mButton.isEnabled()) { mOnClickListener.onClick(dialog, index); } } }); return mButton; } /** * 生成适用于对话框的按钮 */ private QMUIButton generateActionButton(Context context, CharSequence text, int iconRes, int skinBackgroundAttr, int skinTextColorAttr, int iconTintColor) { QMUIButton button = new QMUIButton(context); button.setBackground(null); button.setMinHeight(0); button.setMinimumHeight(0); button.setChangeAlphaWhenDisable(true); button.setChangeAlphaWhenPress(true); TypedArray a = context.obtainStyledAttributes( null, R.styleable.QMUIDialogActionStyleDef, R.attr.qmui_dialog_action_style, 0); int count = a.getIndexCount(); int paddingHor = 0, iconSpace = 0; ColorStateList negativeTextColor = null, positiveTextColor = null; for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogActionStyleDef_android_gravity) { button.setGravity(a.getInt(attr, -1)); } else if (attr == R.styleable.QMUIDialogActionStyleDef_android_textColor) { button.setTextColor(a.getColorStateList(attr)); } else if (attr == R.styleable.QMUIDialogActionStyleDef_android_textSize) { button.setTextSize(TypedValue.COMPLEX_UNIT_PX, a.getDimensionPixelSize(attr, 0)); } else if (attr == R.styleable.QMUIDialogActionStyleDef_qmui_dialog_action_button_padding_horizontal) { paddingHor = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUIDialogActionStyleDef_android_background) { button.setBackground(a.getDrawable(attr)); } else if (attr == R.styleable.QMUIDialogActionStyleDef_android_minWidth) { int miniWidth = a.getDimensionPixelSize(attr, 0); button.setMinWidth(miniWidth); button.setMinimumWidth(miniWidth); } else if (attr == R.styleable.QMUIDialogActionStyleDef_qmui_dialog_positive_action_text_color) { positiveTextColor = a.getColorStateList(attr); } else if (attr == R.styleable.QMUIDialogActionStyleDef_qmui_dialog_negative_action_text_color) { negativeTextColor = a.getColorStateList(attr); } else if (attr == R.styleable.QMUIDialogActionStyleDef_qmui_dialog_action_icon_space) { iconSpace = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUITextCommonStyleDef_android_textStyle) { int styleIndex = a.getInt(attr, -1); button.setTypeface(null, styleIndex); } } a.recycle(); button.setPadding(paddingHor, 0, paddingHor, 0); if (iconRes <= 0) { button.setText(text); } else { button.setText(QMUISpanHelper.generateSideIconText( true, iconSpace, text, ContextCompat.getDrawable(context, iconRes), iconTintColor, button)); } button.setClickable(true); button.setEnabled(mIsEnabled); if (mActionProp == ACTION_PROP_NEGATIVE) { button.setTextColor(negativeTextColor); if (skinTextColorAttr == 0) { skinTextColorAttr = R.attr.qmui_skin_support_dialog_negative_action_text_color; } } else if (mActionProp == ACTION_PROP_POSITIVE) { button.setTextColor(positiveTextColor); if (skinTextColorAttr == 0) { skinTextColorAttr = R.attr.qmui_skin_support_dialog_positive_action_text_color; } } else { if (skinTextColorAttr == 0) { skinTextColorAttr = R.attr.qmui_skin_support_dialog_action_text_color; } } QMUISkinValueBuilder skinValueBuilder = QMUISkinValueBuilder.acquire(); skinBackgroundAttr = skinBackgroundAttr == 0 ? R.attr.qmui_skin_support_dialog_action_bg : skinBackgroundAttr; skinValueBuilder.background(skinBackgroundAttr); skinValueBuilder.textColor(skinTextColorAttr); if(mSkinSeparatorColorAttr != 0){ skinValueBuilder.topSeparator(mSkinSeparatorColorAttr); skinValueBuilder.leftSeparator(mSkinSeparatorColorAttr); } QMUISkinHelper.setSkinValue(button, skinValueBuilder); skinValueBuilder.release(); return button; } public int getActionProp() { return mActionProp; } public interface ActionListener { void onClick(QMUIDialog dialog, int index); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogBlockBuilder.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.widget.dialog; import android.content.Context; import android.content.res.TypedArray; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUIWrapContentScrollView; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import androidx.annotation.Nullable; /** * @author cginechen * @date 2015-12-12 */ public class QMUIDialogBlockBuilder extends QMUIDialogBuilder { private CharSequence mContent; public QMUIDialogBlockBuilder(Context context) { super(context); setActionDivider(1, R.attr.qmui_skin_support_dialog_action_divider_color, 0, 0); } public QMUIDialogBlockBuilder setContent(CharSequence content) { mContent = content; return this; } public QMUIDialogBlockBuilder setContent(int contentRes) { mContent = getBaseContext().getResources().getString(contentRes); return this; } @Nullable @Override protected View onCreateTitle(QMUIDialog dialog, QMUIDialogView parent, Context context) { View result = super.onCreateTitle(dialog, parent, context); if(result != null && (mContent == null || mContent.length() == 0)){ TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogTitleTvCustomDef, R.attr.qmui_dialog_title_style, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogTitleTvCustomDef_qmui_paddingBottomWhenNotContent) { result.setPadding( result.getPaddingLeft(), result.getPaddingTop(), result.getPaddingRight(), a.getDimensionPixelSize(attr, result.getPaddingBottom()) ); } } a.recycle(); } return result; } @Override @Nullable protected View onCreateContent(QMUIDialog dialog, QMUIDialogView parent, Context context) { if(mContent != null && mContent.length() > 0){ TextView contentTv = new QMUISpanTouchFixTextView(context); QMUIResHelper.assignTextViewWithAttr(contentTv, R.attr.qmui_dialog_message_content_style); if (!hasTitle()) { TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogMessageTvCustomDef, R.attr.qmui_dialog_message_content_style, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMessageTvCustomDef_qmui_paddingTopWhenNotTitle) { contentTv.setPadding( contentTv.getPaddingLeft(), a.getDimensionPixelSize(attr, contentTv.getPaddingTop()), contentTv.getPaddingRight(), contentTv.getPaddingBottom() ); } } a.recycle(); } contentTv.setText(mContent); return wrapWithScroll(contentTv); } return null; } @Override public QMUIDialog create(int style) { setActionContainerOrientation(VERTICAL); return super.create(style); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogBuilder.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.widget.dialog; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.Space; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIButton; import com.qmuiteam.qmui.layout.QMUILinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUIWrapContentScrollView; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.constraintlayout.widget.ConstraintLayout; /** * 创建 {@link QMUIDialog} 的 Builder 基类, 不同的 Builder 子类拥有创建不同类型对话框的能力, 具体见子类。 *

该类产生的 Dialog 分为上中下三个部分:

*
    *
  • 上部分是 title 区域, 支持显示纯文本标题, 通过 {@link #setTitle(int)} 系列方法设置。 * 子类也可以通过 override {@link #onCreateTitle(QMUIDialog, QMUIDialogView, Context)} 方法自定义
  • *
  • 中间部分的内容由各个子类决定, 子类通过 override {@link #onCreateContent(QMUIDialog, QMUIDialogView, Context)} 方法自定义。
  • *
  • 下部分是操作区域, 支持添加操作按钮, 通过 {@link #addAction(int, int, QMUIDialogAction.ActionListener)} 系列方法添加。 * 子类也可以通过 override {@link #onCreateOperatorLayout(QMUIDialog, QMUIDialogView, Context)} 方法自定义。 * 其中操作按钮有内联和块级之分, 也有普通、正向、反向之分, 具体见 {@link QMUIDialogAction} *
  • *
* * @author cginechen * @date 2015-10-20 */ public abstract class QMUIDialogBuilder { @IntDef({HORIZONTAL, VERTICAL}) @Retention(RetentionPolicy.SOURCE) public @interface Orientation { } public static final int HORIZONTAL = 0; public static final int VERTICAL = 1; /** * A global theme provider, use to distinguish theme from different builder type */ private static OnProvideDefaultTheme sOnProvideDefaultTheme = null; public static void setOnProvideDefaultTheme(OnProvideDefaultTheme onProvideDefaultTheme) { QMUIDialogBuilder.sOnProvideDefaultTheme = onProvideDefaultTheme; } private Context mContext; protected QMUIDialog mDialog; protected String mTitle; private boolean mCancelable = true; private boolean mCanceledOnTouchOutside = true; protected QMUIDialogRootLayout mRootView; protected QMUIDialogView mDialogView; protected List mActions = new ArrayList<>(); private QMUIDialogView.OnDecorationListener mOnDecorationListener; @Orientation private int mActionContainerOrientation = HORIZONTAL; private boolean mChangeAlphaForPressOrDisable = true; private int mActionDividerThickness = 0; private int mActionDividerColorAttr = R.attr.qmui_skin_support_dialog_action_divider_color; private int mActionDividerInsetStart = 0; private int mActionDividerInsetEnd = 0; private int mActionDividerColor = 0; private boolean mCheckKeyboardOverlay = false; private QMUISkinManager mSkinManager; private float mMaxPercent = 0.75f; public QMUIDialogBuilder(Context context) { this.mContext = context; } public Context getBaseContext() { return mContext; } /** * 设置对话框顶部的标题文字 */ @SuppressWarnings("unchecked") public T setTitle(String title) { if (title != null && title.length() > 0) { this.mTitle = title + mContext.getString(R.string.qmui_tool_fixellipsize); } return (T) this; } /** * 设置对话框顶部的标题文字 */ public T setTitle(int resId) { return setTitle(mContext.getResources().getString(resId)); } @SuppressWarnings("unchecked") public T setCancelable(boolean cancelable) { mCancelable = cancelable; return (T) this; } @SuppressWarnings("unchecked") public T setCanceledOnTouchOutside(boolean canceledOnTouchOutside) { mCanceledOnTouchOutside = canceledOnTouchOutside; return (T) this; } @SuppressWarnings("unchecked") public T setOnDecorationListener(QMUIDialogView.OnDecorationListener onDecorationListener) { mOnDecorationListener = onDecorationListener; return (T) this; } @SuppressWarnings("unchecked") public T setActionContainerOrientation(int actionContainerOrientation) { mActionContainerOrientation = actionContainerOrientation; return (T) this; } @SuppressWarnings("unchecked") public T setChangeAlphaForPressOrDisable(boolean changeAlphaForPressOrDisable) { mChangeAlphaForPressOrDisable = changeAlphaForPressOrDisable; return (T) this; } @SuppressWarnings("unchecked") public T setActionDivider(int thickness, int colorAttr, int startInset, int endInset) { mActionDividerThickness = thickness; mActionDividerColorAttr = colorAttr; mActionDividerInsetStart = startInset; mActionDividerInsetEnd = endInset; return (T) this; } @SuppressWarnings("unchecked") public T setActionDividerInsetAndThickness(int thickness, int startInset, int endInset){ mActionDividerThickness = thickness; mActionDividerInsetStart = startInset; mActionDividerInsetEnd = endInset; return (T) this; } @SuppressWarnings("unchecked") public T setActionDividerColorAttr(int colorAttr){ mActionDividerColorAttr = colorAttr; return (T) this; } @SuppressWarnings("unchecked") public T setActionDividerColor(int color){ mActionDividerColor = color; mActionDividerColorAttr = 0; return (T) this; } @SuppressWarnings("unchecked") public T setCheckKeyboardOverlay(boolean checkKeyboardOverlay) { mCheckKeyboardOverlay = checkKeyboardOverlay; return (T) this; } @SuppressWarnings("unchecked") public T setSkinManager(@Nullable QMUISkinManager skinManager) { mSkinManager = skinManager; return (T) this; } @SuppressWarnings("unchecked") public T setMaxPercent(float maxPercent) { mMaxPercent = maxPercent; return (T) this; } //region 添加action /** * 添加对话框底部的操作按钮 */ @SuppressWarnings("unchecked") public T addAction(@Nullable QMUIDialogAction action) { if (action != null) { mActions.add(action); } return (T) this; } /** * 添加无图标正常类型的操作按钮 * * @param strResId 文案 * @param listener 点击回调事件 */ public T addAction(int strResId, QMUIDialogAction.ActionListener listener) { return addAction(0, strResId, listener); } /** * 添加无图标正常类型的操作按钮 * * @param str 文案 * @param listener 点击回调事件 */ public T addAction(CharSequence str, QMUIDialogAction.ActionListener listener) { return addAction(0, str, QMUIDialogAction.ACTION_PROP_NEUTRAL, listener); } /** * 添加普通类型的操作按钮 * * @param iconResId 图标 * @param strResId 文案 * @param listener 点击回调事件 */ public T addAction(int iconResId, int strResId, QMUIDialogAction.ActionListener listener) { return addAction(iconResId, strResId, QMUIDialogAction.ACTION_PROP_NEUTRAL, listener); } /** * 添加普通类型的操作按钮 * * @param iconResId 图标 * @param str 文案 * @param listener 点击回调事件 */ public T addAction(int iconResId, CharSequence str, QMUIDialogAction.ActionListener listener) { return addAction(iconResId, str, QMUIDialogAction.ACTION_PROP_NEUTRAL, listener); } /** * 添加操作按钮 * * @param iconRes 图标 * @param strRes 文案 * @param prop 属性 * @param listener 点击回调事件 */ public T addAction(int iconRes, int strRes, @QMUIDialogAction.Prop int prop, QMUIDialogAction.ActionListener listener) { return addAction(iconRes, mContext.getResources().getString(strRes), prop, listener); } /** * 添加操作按钮 * * @param iconRes 图标 * @param str 文案 * @param prop 属性 * @param listener 点击回调事件 */ @SuppressWarnings("unchecked") public T addAction(int iconRes, CharSequence str, @QMUIDialogAction.Prop int prop, QMUIDialogAction.ActionListener listener) { QMUIDialogAction action = new QMUIDialogAction(str) .iconRes(iconRes) .prop(prop) .onClick(listener); mActions.add(action); return (T) this; } //endregion /** * 判断对话框是否需要显示title * * @return 是否有title */ protected boolean hasTitle() { return mTitle != null && mTitle.length() != 0; } /** * 产生一个 Dialog 并显示出来 */ public QMUIDialog show() { final QMUIDialog dialog = create(); dialog.show(); return dialog; } /** * 只产生一个 Dialog, 不显示出来 * * @see #create(int) */ public QMUIDialog create() { if (sOnProvideDefaultTheme != null) { int theme = sOnProvideDefaultTheme.getThemeForBuilder(this); if (theme > 0) { return create(theme); } } return create(R.style.QMUI_Dialog); } /** * 产生一个Dialog,但不显示出来。 * * @param style Dialog 的样式 * @see #create() */ @SuppressLint("InflateParams") public QMUIDialog create(@StyleRes int style) { mDialog = new QMUIDialog(mContext, style); Context dialogContext = mDialog.getContext(); mDialogView = onCreateDialogView(dialogContext); mRootView = new QMUIDialogRootLayout(dialogContext, mDialogView, onCreateDialogLayoutParams()); mRootView.setCheckKeyboardOverlay(mCheckKeyboardOverlay); mRootView.setOverlayOccurInMeasureCallback(new QMUIDialogRootLayout.OverlayOccurInMeasureCallback() { @Override public void call() { onOverlayOccurredInMeasure(); } }); mRootView.setMaxPercent(mMaxPercent); configRootLayout(mRootView); mDialogView = mRootView.getDialogView(); mDialogView.setOnDecorationListener(mOnDecorationListener); // title View titleView = onCreateTitle(mDialog, mDialogView, dialogContext); View operatorLayout = onCreateOperatorLayout(mDialog, mDialogView, dialogContext); View contentLayout = onCreateContent(mDialog, mDialogView, dialogContext); checkAndSetId(titleView, R.id.qmui_dialog_title_id); checkAndSetId(operatorLayout, R.id.qmui_dialog_operator_layout_id); checkAndSetId(contentLayout, R.id.qmui_dialog_content_id); // chain if (titleView != null) { ConstraintLayout.LayoutParams lp = onCreateTitleLayoutParams(dialogContext); if (contentLayout != null) { lp.bottomToTop = contentLayout.getId(); } else if (operatorLayout != null) { lp.bottomToTop = operatorLayout.getId(); } else { lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; } mDialogView.addView(titleView, lp); } if (contentLayout != null) { ConstraintLayout.LayoutParams lp = onCreateContentLayoutParams(dialogContext); if (titleView != null) { lp.topToBottom = titleView.getId(); } else { lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; } if (operatorLayout != null) { lp.bottomToTop = operatorLayout.getId(); } else { lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; } mDialogView.addView(contentLayout, lp); } if (operatorLayout != null) { ConstraintLayout.LayoutParams lp = onCreateOperatorLayoutLayoutParams(dialogContext); if (contentLayout != null) { lp.topToBottom = contentLayout.getId(); } else if (titleView != null) { lp.topToBottom = titleView.getId(); } else { lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; } mDialogView.addView(operatorLayout, lp); } mDialog.addContentView(mRootView, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mDialog.setCancelable(mCancelable); mDialog.setCanceledOnTouchOutside(mCanceledOnTouchOutside); mDialog.setSkinManager(mSkinManager); onAfterCreate(mDialog, mRootView, dialogContext); return mDialog; } protected void onAfterCreate(@NonNull QMUIDialog dialog, @NonNull QMUIDialogRootLayout rootLayout, @NonNull Context context){ } protected void onOverlayOccurredInMeasure(){ } private void checkAndSetId(@Nullable View view, int id) { if (view != null && view.getId() == View.NO_ID) { view.setId(id); } } protected void configRootLayout(@NonNull QMUIDialogRootLayout rootLayout){ } protected void skinConfigDialogView(QMUIDialogView dialogView){ QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.background(R.attr.qmui_skin_support_dialog_bg); QMUISkinHelper.setSkinValue(dialogView, valueBuilder); QMUISkinValueBuilder.release(valueBuilder); } protected void skinConfigTitleView(TextView titleView){ QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.textColor(R.attr.qmui_skin_support_dialog_title_text_color); QMUISkinHelper.setSkinValue(titleView, valueBuilder); QMUISkinValueBuilder.release(valueBuilder); } protected void skinConfigActionContainer(ViewGroup actionContainer){ QMUISkinValueBuilder valueBuilder = QMUISkinValueBuilder.acquire(); valueBuilder.topSeparator(R.attr.qmui_skin_support_dialog_action_container_separator_color); QMUISkinHelper.setSkinValue(actionContainer, valueBuilder); QMUISkinValueBuilder.release(valueBuilder); } @NonNull protected QMUIDialogView onCreateDialogView(@NonNull Context context){ QMUIDialogView dialogView = new QMUIDialogView(context); dialogView.setBackground(QMUIResHelper.getAttrDrawable(context, R.attr.qmui_skin_support_dialog_bg)); dialogView.setRadius(QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_radius)); skinConfigDialogView(dialogView); return dialogView; } @NonNull protected FrameLayout.LayoutParams onCreateDialogLayoutParams() { return new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } @Nullable protected View onCreateTitle(@NonNull QMUIDialog dialog, @NonNull QMUIDialogView parent, @NonNull Context context) { if (hasTitle()) { TextView tv = new QMUISpanTouchFixTextView(context); tv.setId(R.id.qmui_dialog_title_id); tv.setText(mTitle); QMUIResHelper.assignTextViewWithAttr(tv, R.attr.qmui_dialog_title_style); skinConfigTitleView(tv); return tv; } return null; } @NonNull protected ConstraintLayout.LayoutParams onCreateTitleLayoutParams(@NonNull Context context) { ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; lp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; lp.verticalChainStyle = ConstraintLayout.LayoutParams.CHAIN_PACKED; return lp; } @Nullable protected abstract View onCreateContent(@NonNull QMUIDialog dialog, @NonNull QMUIDialogView parent, @NonNull Context context); protected QMUIWrapContentScrollView wrapWithScroll(@NonNull View view){ QMUIWrapContentScrollView scrollView = new QMUIWrapContentScrollView(view.getContext()); scrollView.addView(view); scrollView.setVerticalScrollBarEnabled(false); return scrollView; } protected ConstraintLayout.LayoutParams onCreateContentLayoutParams(@NonNull Context context) { ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; lp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; lp.constrainedHeight = true; return lp; } @Nullable protected View onCreateOperatorLayout(@NonNull final QMUIDialog dialog, @NonNull QMUIDialogView parent, @NonNull Context context) { int size = mActions.size(); if (size > 0) { TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogActionContainerCustomDef, R.attr.qmui_dialog_action_container_style, 0); int count = a.getIndexCount(); int justifyContent = 1, spaceCustomIndex = 0; int actionHeight = -1, actionSpace = 0; for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogActionContainerCustomDef_qmui_dialog_action_container_justify_content) { justifyContent = a.getInteger(attr, justifyContent); } else if (attr == R.styleable.QMUIDialogActionContainerCustomDef_qmui_dialog_action_container_custom_space_index) { spaceCustomIndex = a.getInteger(attr, 0); } else if (attr == R.styleable.QMUIDialogActionContainerCustomDef_qmui_dialog_action_space) { actionSpace = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUIDialogActionContainerCustomDef_qmui_dialog_action_height) { actionHeight = a.getDimensionPixelSize(attr, 0); } } a.recycle(); int spaceInsertPos = -1; if (mActionContainerOrientation != VERTICAL) { if (justifyContent == 0) { spaceInsertPos = size; } else if (justifyContent == 1) { spaceInsertPos = 0; } else if (justifyContent == 3) { spaceInsertPos = spaceCustomIndex; } } final QMUILinearLayout layout = new QMUILinearLayout(context, null, R.attr.qmui_dialog_action_container_style); layout.setId(R.id.qmui_dialog_operator_layout_id); layout.setOrientation(mActionContainerOrientation == VERTICAL ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); skinConfigActionContainer(layout); for (int i = 0; i < size; i++) { if (spaceInsertPos == i) { layout.addView(createActionContainerSpace(context)); } QMUIDialogAction action = mActions.get(i); action.skinSeparatorColorAttr(mActionDividerColorAttr); LinearLayout.LayoutParams actionLp; if (mActionContainerOrientation == VERTICAL) { actionLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, actionHeight); } else { actionLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, actionHeight); if (spaceInsertPos >= 0) { if (i >= spaceInsertPos) { actionLp.leftMargin = actionSpace; } else { actionLp.rightMargin = actionSpace; } } if (justifyContent == 2) { actionLp.weight = 1; } } QMUIButton actionView = action.buildActionView(mDialog, i); // add divider if (mActionDividerThickness > 0 && i > 0 && spaceInsertPos != i) { int color = mActionDividerColorAttr == 0 ? mActionDividerColor : QMUISkinHelper.getSkinColor(actionView, mActionDividerColorAttr); if (mActionContainerOrientation == VERTICAL) { actionView.onlyShowTopDivider(mActionDividerInsetStart, mActionDividerInsetEnd, mActionDividerThickness, color); } else { actionView.onlyShowLeftDivider(mActionDividerInsetStart, mActionDividerInsetEnd, mActionDividerThickness, color); } } actionView.setChangeAlphaWhenDisable(mChangeAlphaForPressOrDisable); actionView.setChangeAlphaWhenPress(mChangeAlphaForPressOrDisable); layout.addView(actionView, actionLp); } if (spaceInsertPos == size) { layout.addView(createActionContainerSpace(context)); } if (mActionContainerOrientation == HORIZONTAL) { layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int width = right - left; int childCount = layout.getChildCount(); if (childCount > 0) { View lastChild = layout.getChildAt(childCount - 1); // 如果ActionButton的宽度过宽,则减小padding if (lastChild.getRight() > width) { int childPaddingHor = Math.max(0, lastChild.getPaddingLeft() - QMUIDisplayHelper.dp2px(mContext, 3)); for (int i = 0; i < childCount; i++) { layout.getChildAt(i).setPadding(childPaddingHor, 0, childPaddingHor, 0); } } } } }); } return layout; } return null; } @NonNull protected ConstraintLayout.LayoutParams onCreateOperatorLayoutLayoutParams(@NonNull Context context) { ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; lp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; lp.verticalChainStyle = ConstraintLayout.LayoutParams.CHAIN_PACKED; return lp; } private View createActionContainerSpace(Context context) { Space space = new Space(context); LinearLayout.LayoutParams spaceLp = new LinearLayout.LayoutParams(0, 0); spaceLp.weight = 1; space.setLayoutParams(spaceLp); return space; } public List getPositiveAction() { List output = new ArrayList<>(); for (QMUIDialogAction action : mActions) { if (action.getActionProp() == QMUIDialogAction.ACTION_PROP_POSITIVE) { output.add(action); } } return output; } public interface OnProvideDefaultTheme { int getThemeForBuilder(QMUIDialogBuilder builder); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogMenuItemView.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.widget.dialog; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.text.TextUtils; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import androidx.appcompat.widget.AppCompatImageView; /** * 菜单类型的对话框的item * * @author chantchen * @date 2016-1-20 */ public class QMUIDialogMenuItemView extends QMUIConstraintLayout { private int index = -1; private MenuItemViewListener mListener; private boolean mIsChecked = false; public QMUIDialogMenuItemView(Context context) { super(context, null, R.attr.qmui_dialog_menu_item_style); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.background(R.attr.qmui_skin_support_s_dialog_menu_item_bg); QMUISkinHelper.setSkinValue(this, builder); QMUISkinValueBuilder.release(builder); } @SuppressLint("CustomViewStyleable") public static TextView createItemTextView(Context context) { TextView tv = new QMUISpanTouchFixTextView(context); TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogMenuTextStyleDef, R.attr.qmui_dialog_menu_item_style, 0); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMenuTextStyleDef_android_gravity) { tv.setGravity(a.getInt(attr, -1)); } else if (attr == R.styleable.QMUIDialogMenuTextStyleDef_android_textColor) { tv.setTextColor(a.getColorStateList(attr)); } else if (attr == R.styleable.QMUIDialogMenuTextStyleDef_android_textSize) { tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, a.getDimensionPixelSize(attr, 0)); } } a.recycle(); tv.setId(View.generateViewId()); tv.setSingleLine(true); tv.setEllipsize(TextUtils.TruncateAt.MIDDLE); tv.setDuplicateParentStateEnabled(false); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.textColor(R.attr.qmui_skin_support_dialog_menu_item_text_color); QMUISkinHelper.setSkinValue(tv, builder); QMUISkinValueBuilder.release(builder); return tv; } public int getMenuIndex() { return this.index; } public void setMenuIndex(int index) { this.index = index; } protected void notifyCheckChange(boolean isChecked) { } public boolean isChecked() { return mIsChecked; } public void setChecked(boolean checked) { mIsChecked = checked; notifyCheckChange(mIsChecked); } public void setListener(MenuItemViewListener listener) { if (!isClickable()) { setClickable(true); } mListener = listener; } @Override public boolean performClick() { if (mListener != null) { mListener.onClick(index); } return super.performClick(); } public interface MenuItemViewListener { void onClick(int index); } public static class TextItemView extends QMUIDialogMenuItemView { protected TextView mTextView; public TextItemView(Context context) { super(context); init(); } public TextItemView(Context context, CharSequence text) { super(context); init(); setText(text); } private void init() { mTextView = createItemTextView(getContext()); LayoutParams lp = new LayoutParams(0, 0); lp.leftToLeft = LayoutParams.PARENT_ID; lp.rightToRight = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; lp.topToTop = LayoutParams.PARENT_ID; addView(mTextView, lp); } public void setText(CharSequence text) { mTextView.setText(text); } @Deprecated public void setTextColor(int color) { mTextView.setTextColor(color); } public void setTextColorAttr(int colorAttr) { int color = QMUISkinHelper.getSkinColor(this, colorAttr); mTextView.setTextColor(color); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.textColor(colorAttr); QMUISkinHelper.setSkinValue(mTextView, builder); QMUISkinValueBuilder.release(builder); } } public static class MarkItemView extends QMUIDialogMenuItemView { private Context mContext; private TextView mTextView; private AppCompatImageView mCheckedView; @SuppressLint("CustomViewStyleable") public MarkItemView(Context context) { super(context); mContext = context; mCheckedView = new AppCompatImageView(mContext); mCheckedView.setId(View.generateViewId()); TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogMenuMarkDef, R.attr.qmui_dialog_menu_item_style, 0); int markMarginHor = 0; int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMenuMarkDef_qmui_dialog_menu_item_check_mark_margin_hor) { markMarginHor = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUIDialogMenuMarkDef_qmui_dialog_menu_item_mark_drawable) { mCheckedView.setImageDrawable(QMUIResHelper.getAttrDrawable(context, a, attr)); } } a.recycle(); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.src(R.attr.qmui_skin_support_dialog_mark_drawable); QMUISkinHelper.setSkinValue(mCheckedView, builder); QMUISkinValueBuilder.release(builder); LayoutParams checkLp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); checkLp.rightToRight = LayoutParams.PARENT_ID; checkLp.topToTop = LayoutParams.PARENT_ID; checkLp.bottomToBottom = LayoutParams.PARENT_ID; addView(mCheckedView, checkLp); mTextView = createItemTextView(mContext); LayoutParams tvLp = new LayoutParams(0, 0); tvLp.leftToLeft = LayoutParams.PARENT_ID; tvLp.topToTop = LayoutParams.PARENT_ID; tvLp.bottomToBottom = LayoutParams.PARENT_ID; tvLp.rightToLeft = mCheckedView.getId(); tvLp.rightMargin = markMarginHor; addView(mTextView, tvLp); mCheckedView.setVisibility(INVISIBLE); } public MarkItemView(Context context, CharSequence text) { this(context); setText(text); } public void setText(CharSequence text) { mTextView.setText(text); } @Override protected void notifyCheckChange(boolean isChecked) { mCheckedView.setVisibility(isChecked ? VISIBLE : INVISIBLE); } } @SuppressLint({"ViewConstructor", "CustomViewStyleable"}) public static class CheckItemView extends QMUIDialogMenuItemView { private Context mContext; private TextView mTextView; private AppCompatImageView mCheckedView; public CheckItemView(Context context, boolean right) { super(context); mContext = context; mCheckedView = new AppCompatImageView(mContext); mCheckedView.setId(View.generateViewId()); TypedArray a = context.obtainStyledAttributes(null, R.styleable.QMUIDialogMenuCheckDef, R.attr.qmui_dialog_menu_item_style, 0); int markMarginHor = 0; int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); if (attr == R.styleable.QMUIDialogMenuCheckDef_qmui_dialog_menu_item_check_mark_margin_hor) { markMarginHor = a.getDimensionPixelSize(attr, 0); } else if (attr == R.styleable.QMUIDialogMenuCheckDef_qmui_dialog_menu_item_check_drawable) { mCheckedView.setImageDrawable(QMUIResHelper.getAttrDrawable(context, a, attr)); } } a.recycle(); LayoutParams checkLp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); checkLp.topToTop = LayoutParams.PARENT_ID; checkLp.bottomToBottom = LayoutParams.PARENT_ID; if(right){ checkLp.rightToRight = LayoutParams.PARENT_ID; }else{ checkLp.leftToLeft = LayoutParams.PARENT_ID; } QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.src(R.attr.qmui_skin_support_s_dialog_check_drawable); QMUISkinHelper.setSkinValue(mCheckedView, builder); QMUISkinValueBuilder.release(builder); addView(mCheckedView, checkLp); mTextView = createItemTextView(mContext); LayoutParams tvLp = new LayoutParams(0, 0); if(right){ tvLp.leftToLeft = LayoutParams.PARENT_ID; tvLp.rightToLeft = mCheckedView.getId(); tvLp.rightMargin = markMarginHor; }else{ tvLp.rightToRight = LayoutParams.PARENT_ID; tvLp.leftToRight = mCheckedView.getId(); tvLp.leftMargin = markMarginHor; } tvLp.topToTop = LayoutParams.PARENT_ID; tvLp.bottomToBottom = LayoutParams.PARENT_ID; addView(mTextView, tvLp); } public CheckItemView(Context context, boolean right, CharSequence text) { this(context, right); setText(text); } public void setText(CharSequence text) { mTextView.setText(text); } public CharSequence getText() { return mTextView.getText(); } @Override protected void notifyCheckChange(boolean isChecked) { QMUIViewHelper.safeSetImageViewSelected(mCheckedView, isChecked); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogRootLayout.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.widget.dialog; import android.content.Context; import android.graphics.Rect; import android.view.MotionEvent; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIWindowHelper; public class QMUIDialogRootLayout extends ViewGroup { private QMUIDialogView mDialogView; private FrameLayout.LayoutParams mDialogViewLp; private int mMinWidth; private int mMaxWidth; private int mInsetHor; private int mInsetVer; private boolean mCheckKeyboardOverlay = false; private float mMaxPercent = 0.75f; private boolean isOverlayOccurEventNotified = false; private OverlayOccurInMeasureCallback mOverlayOccurInMeasureCallback; private int mLastContentInsetTop = 0; public QMUIDialogRootLayout(@NonNull Context context, @NonNull QMUIDialogView dialogView, @Nullable FrameLayout.LayoutParams dialogViewLp) { super(context); mDialogView = dialogView; if (dialogViewLp == null) { dialogViewLp = new FrameLayout.LayoutParams( LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } mDialogViewLp = dialogViewLp; addView(mDialogView, dialogViewLp); mMinWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_min_width); mMaxWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_max_width); mInsetHor = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_inset_hor); mInsetVer = QMUIResHelper.getAttrDimen(context, R.attr.qmui_dialog_inset_ver); setId(R.id.qmui_dialog_root_layout); } public void setMinWidth(int minWidth) { mMinWidth = minWidth; } public void setMaxWidth(int maxWidth) { mMaxWidth = maxWidth; } public void setInsetHor(int insetHor) { mInsetHor = insetHor; } public void setInsetVer(int insetVer) { mInsetVer = insetVer; } public void setOverlayOccurInMeasureCallback(OverlayOccurInMeasureCallback overlayOccurInMeasureCallback) { mOverlayOccurInMeasureCallback = overlayOccurInMeasureCallback; } public void setCheckKeyboardOverlay(boolean checkKeyboardOverlay) { mCheckKeyboardOverlay = checkKeyboardOverlay; } public void setMaxPercent(float maxPercent) { mMaxPercent = maxPercent; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int keyboardOverlayHeight = 0; int contentInsetVer = 0; if (mCheckKeyboardOverlay) { Rect visibleInsetRect = QMUIWindowHelper.unSafeGetWindowVisibleInsets(this); Rect contentInsetRect = QMUIWindowHelper.unSafeGetContentInsets(this); if (visibleInsetRect != null) { keyboardOverlayHeight = visibleInsetRect.bottom; } if (contentInsetRect != null) { mLastContentInsetTop = contentInsetRect.top; contentInsetVer = contentInsetRect.top + contentInsetRect.bottom; } } int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int childWidthMeasureSpec, childHeightMeasureSpec; if (mDialogViewLp.width > 0) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mDialogViewLp.width, MeasureSpec.EXACTLY); } else { int childMaxWidth = Math.min(mMaxWidth, widthSize - 2 * mInsetHor); if (childMaxWidth <= mMinWidth) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMinWidth, MeasureSpec.EXACTLY); } else if (mDialogViewLp.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.AT_MOST); } } if (mDialogViewLp.height > 0) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mDialogViewLp.height, MeasureSpec.EXACTLY); } else { int childMaxHeight; if (keyboardOverlayHeight > 0) { if (getRootView() != null && getRootView().getHeight() > 0) { // the overlay occurred with this height, we can't change it. heightSize = getRootView().getHeight(); if (!isOverlayOccurEventNotified) { isOverlayOccurEventNotified = true; if (mOverlayOccurInMeasureCallback != null) { mOverlayOccurInMeasureCallback.call(); } } } childMaxHeight = Math.max(heightSize - 2 * mInsetVer - keyboardOverlayHeight - contentInsetVer, 0); } else { // use maxPercent to keep dialog from being too high and calculated based on // screen height because height size while change to actual height when multi onMeasure. isOverlayOccurEventNotified = false; childMaxHeight = Math.min(heightSize - 2 * mInsetVer - contentInsetVer, (int) (QMUIDisplayHelper.getScreenHeight(getContext()) * mMaxPercent - 2 * mInsetVer)); } if (mDialogViewLp.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childMaxHeight, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childMaxHeight, MeasureSpec.AT_MOST); } } mDialogView.measure(childWidthMeasureSpec, childHeightMeasureSpec); if (mDialogView.getMeasuredWidth() < mMinWidth) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMinWidth, MeasureSpec.EXACTLY); mDialogView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // InsetVer works when keyboard overlay occurs setMeasuredDimension(mDialogView.getMeasuredWidth(), mDialogView.getMeasuredHeight() + 2 * mInsetVer + keyboardOverlayHeight + contentInsetVer); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int w = r - l; int childLeft = (w - mDialogView.getMeasuredWidth()) / 2; mDialogView.layout(childLeft, mInsetVer, childLeft + mDialogView.getMeasuredWidth(), mInsetVer + mDialogView.getMeasuredHeight()); } public QMUIDialogView getDialogView() { return mDialogView; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { // I think this is a android system bug: // When show keyboard In fullscreen and the content overlaps with keyboard, // then the mAttachInfo.mContentInset.top equals notch's height // but the event's y and draw position has different behavior if notch exist. if (mLastContentInsetTop > 0) { ev.offsetLocation(0, -mLastContentInsetTop); } return super.dispatchTouchEvent(ev); } interface OverlayOccurInMeasureCallback { void call(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUIDialogView.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.widget.dialog; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; /** * Created by cgspine on 2018/2/28. */ public class QMUIDialogView extends QMUIConstraintLayout { private OnDecorationListener mOnDecorationListener; public QMUIDialogView(Context context) { this(context, null); } public QMUIDialogView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public QMUIDialogView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setId(R.id.qmui_dialog_layout); } public void setOnDecorationListener(OnDecorationListener onDecorationListener) { mOnDecorationListener = onDecorationListener; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mOnDecorationListener != null) { mOnDecorationListener.onDraw(canvas, this); } } @Override public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mOnDecorationListener != null) { mOnDecorationListener.onDrawOver(canvas, this); } } public interface OnDecorationListener { void onDraw(Canvas canvas, QMUIDialogView view); void onDrawOver(Canvas canvas, QMUIDialogView view); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialog.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.widget.dialog; import android.app.Dialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.LayoutRes; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUILoadingView; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 提供一个浮层展示在屏幕中间, 一般使用 {@link QMUITipDialog.Builder} 或 {@link QMUITipDialog.CustomBuilder} 生成。 *
    *
  • {@link QMUITipDialog.Builder} 提供了一个图标和一行文字的样式, 其中图标有几种类型可选, 见 {@link QMUITipDialog.Builder.IconType}
  • *
  • {@link QMUITipDialog.CustomBuilder} 支持传入自定义的 layoutResId, 达到自定义 TipDialog 的效果。
  • *
* * @author cginechen * @date 2016-10-14 */ public class QMUITipDialog extends QMUIBaseDialog { public QMUITipDialog(Context context) { this(context, R.style.QMUI_TipDialog); } public QMUITipDialog(Context context, int themeResId) { super(context, themeResId); setCanceledOnTouchOutside(false); } /** * 生成默认的 {@link QMUITipDialog} *

* 提供了一个图标和一行文字的样式, 其中图标有几种类型可选。见 {@link IconType} *

* * @see CustomBuilder */ public static class Builder { /** * 不显示任何icon */ public static final int ICON_TYPE_NOTHING = 0; /** * 显示 Loading 图标 */ public static final int ICON_TYPE_LOADING = 1; /** * 显示成功图标 */ public static final int ICON_TYPE_SUCCESS = 2; /** * 显示失败图标 */ public static final int ICON_TYPE_FAIL = 3; /** * 显示信息图标 */ public static final int ICON_TYPE_INFO = 4; @IntDef({ICON_TYPE_NOTHING, ICON_TYPE_LOADING, ICON_TYPE_SUCCESS, ICON_TYPE_FAIL, ICON_TYPE_INFO}) @Retention(RetentionPolicy.SOURCE) public @interface IconType { } private @IconType int mCurrentIconType = ICON_TYPE_NOTHING; private Context mContext; private CharSequence mTipWord; private QMUISkinManager mSkinManager; public Builder(Context context) { mContext = context; } /** * 设置 icon 显示的内容 * * @see IconType */ public Builder setIconType(@IconType int iconType) { mCurrentIconType = iconType; return this; } /** * 设置显示的文案 */ public Builder setTipWord(CharSequence tipWord) { mTipWord = tipWord; return this; } public Builder setSkinManager(@Nullable QMUISkinManager skinManager) { mSkinManager = skinManager; return this; } public QMUITipDialog create() { return create(true); } public QMUITipDialog create(boolean cancelable) { return create(cancelable, R.style.QMUI_TipDialog); } /** * 创建 Dialog, 但没有弹出来, 如果要弹出来, 请调用返回值的 {@link Dialog#show()} 方法 * * @param cancelable 按系统返回键是否可以取消 * @return 创建的 Dialog */ public QMUITipDialog create(boolean cancelable, int style) { QMUITipDialog dialog = new QMUITipDialog(mContext, style); dialog.setCancelable(cancelable); dialog.setSkinManager(mSkinManager); Context dialogContext = dialog.getContext(); QMUITipDialogView dialogView = new QMUITipDialogView(dialogContext); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (mCurrentIconType == ICON_TYPE_LOADING) { QMUILoadingView loadingView = new QMUILoadingView(dialogContext); loadingView.setColor(QMUIResHelper.getAttrColor( dialogContext, R.attr.qmui_skin_support_tip_dialog_loading_color)); loadingView.setSize(QMUIResHelper.getAttrDimen( dialogContext, R.attr.qmui_tip_dialog_loading_size)); builder.tintColor(R.attr.qmui_skin_support_tip_dialog_loading_color); QMUISkinHelper.setSkinValue(loadingView, builder); dialogView.addView(loadingView, onCreateIconOrLoadingLayoutParams(dialogContext)); } else if (mCurrentIconType == ICON_TYPE_SUCCESS || mCurrentIconType == ICON_TYPE_FAIL || mCurrentIconType == ICON_TYPE_INFO) { ImageView imageView = new AppCompatImageView(dialogContext); builder.clear(); Drawable drawable; if (mCurrentIconType == ICON_TYPE_SUCCESS) { drawable = QMUIResHelper.getAttrDrawable( dialogContext, R.attr.qmui_skin_support_tip_dialog_icon_success_src); builder.src( R.attr.qmui_skin_support_tip_dialog_icon_success_src); } else if (mCurrentIconType == ICON_TYPE_FAIL) { drawable = QMUIResHelper.getAttrDrawable( dialogContext, R.attr.qmui_skin_support_tip_dialog_icon_error_src); builder.src(R.attr.qmui_skin_support_tip_dialog_icon_error_src); } else { drawable = QMUIResHelper.getAttrDrawable( dialogContext, R.attr.qmui_skin_support_tip_dialog_icon_info_src); builder.src(R.attr.qmui_skin_support_tip_dialog_icon_info_src); } imageView.setImageDrawable(drawable); QMUISkinHelper.setSkinValue(imageView, builder); dialogView.addView(imageView, onCreateIconOrLoadingLayoutParams(dialogContext)); } if (mTipWord != null && mTipWord.length() > 0) { TextView tipView = new QMUISpanTouchFixTextView(dialogContext); tipView.setEllipsize(TextUtils.TruncateAt.END); tipView.setId(R.id.qmui_tip_content_id); tipView.setGravity(Gravity.CENTER); tipView.setTextSize(TypedValue.COMPLEX_UNIT_PX, QMUIResHelper.getAttrDimen(dialogContext, R.attr.qmui_tip_dialog_text_size)); tipView.setTextColor(QMUIResHelper.getAttrColor( dialogContext, R.attr.qmui_skin_support_tip_dialog_text_color)); tipView.setText(mTipWord); builder.clear(); builder.textColor(R.attr.qmui_skin_support_tip_dialog_text_color); QMUISkinHelper.setSkinValue(tipView, builder); dialogView.addView(tipView, onCreateTextLayoutParams(dialogContext, mCurrentIconType)); } builder.release(); dialog.setContentView(dialogView); return dialog; } protected LinearLayout.LayoutParams onCreateIconOrLoadingLayoutParams(Context context) { return new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } protected LinearLayout.LayoutParams onCreateTextLayoutParams(Context context, @IconType int iconType) { LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); if (iconType != ICON_TYPE_NOTHING) { lp.topMargin = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_text_margin_top); } return lp; } } /** * CustomBuilder with xml layout */ public static class CustomBuilder { private Context mContext; private int mContentLayoutId; private QMUISkinManager mSkinManager; public CustomBuilder(Context context) { mContext = context; } public CustomBuilder setSkinManager(@Nullable QMUISkinManager skinManager) { mSkinManager = skinManager; return this; } public CustomBuilder setContent(@LayoutRes int layoutId) { mContentLayoutId = layoutId; return this; } public QMUITipDialog create() { QMUITipDialog dialog = new QMUITipDialog(mContext); dialog.setSkinManager(mSkinManager); Context dialogContext = dialog.getContext(); QMUITipDialogView tipDialogView = new QMUITipDialogView(dialogContext); LayoutInflater.from(dialogContext).inflate(mContentLayoutId, tipDialogView, true); dialog.setContentView(tipDialogView); return dialog; } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/dialog/QMUITipDialogView.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.widget.dialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.Gravity; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUILinearLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; public class QMUITipDialogView extends QMUILinearLayout { private final int mMaxWidth; private final int mMiniWidth; private final int mMiniHeight; public QMUITipDialogView(Context context) { super(context); setOrientation(VERTICAL); setGravity(Gravity.CENTER_HORIZONTAL); int radius = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_radius); Drawable background = QMUIResHelper.getAttrDrawable(context, R.attr.qmui_skin_support_tip_dialog_bg); int paddingHor = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_padding_horizontal); int paddingVer = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_padding_vertical); setBackground(background); setPadding(paddingHor, paddingVer, paddingHor, paddingVer); setRadius(radius); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.background(R.attr.qmui_skin_support_tip_dialog_bg); QMUISkinHelper.setSkinValue(this, builder); builder.release(); mMaxWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_max_width); mMiniWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_min_width); mMiniHeight = QMUIResHelper.getAttrDimen(context, R.attr.qmui_tip_dialog_min_height); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); if(widthSize > mMaxWidth){ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); boolean needRemeasure = false; if(getMeasuredWidth() < mMiniWidth){ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMiniWidth, MeasureSpec.EXACTLY); needRemeasure = true; } if(getMeasuredHeight() < mMiniHeight){ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMiniHeight, MeasureSpec.EXACTLY); needRemeasure = true; } if(needRemeasure){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUICommonListItemView.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.widget.grouplist; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.appcompat.widget.AppCompatCheckBox; import androidx.appcompat.widget.AppCompatImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 作为通用列表 {@link QMUIGroupListView} 里的 item 使用,也可以单独使用。 * 支持以下样式: *
    *
  • 通过 {@link #setText(CharSequence)} 设置一行文字
  • *
  • 通过 {@link #setDetailText(CharSequence)} 设置一行说明文字, 并通过 {@link #setOrientation(int)} 设置说明文字的位置, * 也可以在 xml 中使用 {@link R.styleable#QMUICommonListItemView_qmui_orientation} 设置。
  • *
  • 通过 {@link #setAccessoryType(int)} 设置右侧 View 的类型, 可选的类型见 {@link QMUICommonListItemAccessoryType}, * 也可以在 xml 中使用 {@link R.styleable#QMUICommonListItemView_qmui_accessory_type} 设置。
  • *
* * @author chantchen * @date 2015-1-8 */ public class QMUICommonListItemView extends QMUIConstraintLayout { /** * 右侧不显示任何东西 */ public final static int ACCESSORY_TYPE_NONE = 0; /** * 右侧显示一个箭头 */ public final static int ACCESSORY_TYPE_CHEVRON = 1; /** * 右侧显示一个开关 */ public final static int ACCESSORY_TYPE_SWITCH = 2; /** * 自定义右侧显示的 View */ public final static int ACCESSORY_TYPE_CUSTOM = 3; private final static int TIP_SHOW_NOTHING = 0; private final static int TIP_SHOW_RED_POINT = 1; private final static int TIP_SHOW_NEW = 2; /** * detailText 在 title 文字的下方 */ public final static int VERTICAL = 0; /** * detailText 在 item 的右方 */ public final static int HORIZONTAL = 1; /** * TIP 在左边 */ public final static int TIP_POSITION_LEFT = 0; /** * TIP 在右边 */ public final static int TIP_POSITION_RIGHT = 1; @IntDef({ACCESSORY_TYPE_NONE, ACCESSORY_TYPE_CHEVRON, ACCESSORY_TYPE_SWITCH, ACCESSORY_TYPE_CUSTOM}) @Retention(RetentionPolicy.SOURCE) public @interface QMUICommonListItemAccessoryType { } @IntDef({VERTICAL, HORIZONTAL}) @Retention(RetentionPolicy.SOURCE) public @interface QMUICommonListItemOrientation { } @IntDef({TIP_POSITION_LEFT, TIP_POSITION_RIGHT}) @Retention(RetentionPolicy.SOURCE) public @interface QMUICommonListItemTipPosition { } /** * Item 右侧的 View 的类型 */ @QMUICommonListItemAccessoryType private int mAccessoryType; /** * 控制 detailText 是在 title 文字的下方还是 item 的右方 */ private int mOrientation = HORIZONTAL; /** * 控制红点的位置 */ @QMUICommonListItemTipPosition private int mTipPosition = TIP_POSITION_LEFT; protected ImageView mImageView; private ViewGroup mAccessoryView; protected TextView mTextView; protected TextView mDetailTextView; protected CheckBox mSwitch; private ImageView mRedDot; private ImageView mNewTipView; private boolean mDisableSwitchSelf = false; private int mTipShown = TIP_SHOW_NOTHING; public QMUICommonListItemView(Context context) { this(context, null); } public QMUICommonListItemView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUICommonListItemViewStyle); } public QMUICommonListItemView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } protected void init(Context context, AttributeSet attrs, int defStyleAttr) { LayoutInflater.from(context).inflate(R.layout.qmui_common_list_item, this, true); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUICommonListItemView, defStyleAttr, 0); @QMUICommonListItemOrientation int orientation = array.getInt(R.styleable.QMUICommonListItemView_qmui_orientation, HORIZONTAL); @QMUICommonListItemAccessoryType int accessoryType = array.getInt(R.styleable.QMUICommonListItemView_qmui_accessory_type, ACCESSORY_TYPE_NONE); final int initTitleColor = array.getColor(R.styleable.QMUICommonListItemView_qmui_common_list_title_color, 0); final int initDetailColor = array.getColor(R.styleable.QMUICommonListItemView_qmui_common_list_detail_color, 0); array.recycle(); mImageView = findViewById(R.id.group_list_item_imageView); mTextView = findViewById(R.id.group_list_item_textView); mRedDot = findViewById(R.id.group_list_item_tips_dot); mNewTipView = findViewById(R.id.group_list_item_tips_new); mDetailTextView = findViewById(R.id.group_list_item_detailTextView); mTextView.setTextColor(initTitleColor); mDetailTextView.setTextColor(initDetailColor); mAccessoryView = findViewById(R.id.group_list_item_accessoryView); setOrientation(orientation); setAccessoryType(accessoryType); } public void updateImageViewLp(LayoutParamConfig lpConfig) { if (lpConfig != null) { LayoutParams lp = (LayoutParams) mImageView.getLayoutParams(); mImageView.setLayoutParams(lpConfig.onConfig(lp)); } } public void setImageDrawable(Drawable drawable) { if (drawable == null) { mImageView.setVisibility(View.GONE); } else { mImageView.setImageDrawable(drawable); mImageView.setVisibility(View.VISIBLE); } } public void setTipPosition(@QMUICommonListItemTipPosition int tipPosition) { if(mTipPosition != tipPosition){ mTipPosition = tipPosition; updateLayoutParams(); } } public CharSequence getText() { return mTextView.getText(); } public void setText(CharSequence text) { mTextView.setText(text); if (QMUILangHelper.isNullOrEmpty(text)) { mTextView.setVisibility(View.GONE); } else { mTextView.setVisibility(View.VISIBLE); } } /** * 切换是否显示小红点 * * @param isShow 是否显示小红点 */ public void showRedDot(boolean isShow) { int oldTipShown = mTipShown; if(isShow){ mTipShown = TIP_SHOW_RED_POINT; }else if(mTipShown == TIP_SHOW_RED_POINT){ mTipShown = TIP_SHOW_NOTHING; } if(oldTipShown != mTipShown){ updateLayoutParams(); } } /** * 切换是否显示更新提示 * * @param isShow 是否显示更新提示 */ public void showNewTip(boolean isShow) { int oldTipShown = mTipShown; if(isShow){ mTipShown = TIP_SHOW_NEW; }else if(mTipShown == TIP_SHOW_NEW){ mTipShown = TIP_SHOW_NOTHING; } if(oldTipShown != mTipShown){ updateLayoutParams(); } } public CharSequence getDetailText() { return mDetailTextView.getText(); } public void setDetailText(CharSequence text) { mDetailTextView.setText(text); if (QMUILangHelper.isNullOrEmpty(text)) { mDetailTextView.setVisibility(View.GONE); } else { mDetailTextView.setVisibility(View.VISIBLE); } } public int getOrientation() { return mOrientation; } public void setOrientation(@QMUICommonListItemOrientation int orientation) { if (mOrientation == orientation) { return; } mOrientation = orientation; updateLayoutParams(); } private void updateLayoutParams(){ mNewTipView.setVisibility(mTipShown == TIP_SHOW_NEW ? View.VISIBLE : View.GONE); mRedDot.setVisibility(mTipShown == TIP_SHOW_RED_POINT ? View.VISIBLE : View.GONE); LayoutParams titleLp = (LayoutParams) mTextView.getLayoutParams(); LayoutParams detailLp = (LayoutParams) mDetailTextView.getLayoutParams(); LayoutParams newTipLp = (LayoutParams) mNewTipView.getLayoutParams(); LayoutParams redDotLp = (LayoutParams) mRedDot.getLayoutParams(); if (mOrientation == VERTICAL) { mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_title_v_text_size)); mDetailTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_detail_v_text_size)); titleLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; titleLp.bottomToBottom = LayoutParams.UNSET; titleLp.bottomToTop = mDetailTextView.getId(); detailLp.horizontalChainStyle = LayoutParams.UNSET; detailLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; detailLp.leftToLeft = mTextView.getId(); detailLp.leftToRight = LayoutParams.UNSET; detailLp.horizontalBias = 0f; detailLp.topToTop = LayoutParams.UNSET; detailLp.topToBottom = mTextView.getId(); detailLp.leftMargin = 0; detailLp.topMargin = QMUIResHelper.getAttrDimen( getContext(), R.attr.qmui_common_list_item_detail_v_margin_with_title); if(mTipShown == TIP_SHOW_NEW){ if(mTipPosition == TIP_POSITION_LEFT){ updateTipLeftVerRelatedLayoutParam(mNewTipView, newTipLp, titleLp, detailLp); }else{ updateTipRightVerRelatedLayoutParam(mNewTipView, newTipLp, titleLp, detailLp); } }else if(mTipShown == TIP_SHOW_RED_POINT){ if(mTipPosition == TIP_POSITION_LEFT){ updateTipLeftVerRelatedLayoutParam(mRedDot, redDotLp, titleLp, detailLp); }else{ updateTipRightVerRelatedLayoutParam(mRedDot, redDotLp, titleLp, detailLp); } }else{ int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); titleLp.horizontalChainStyle = LayoutParams.UNSET; titleLp.rightToLeft = mAccessoryView.getId(); titleLp.rightMargin = accessoryLeftMargin; titleLp.goneRightMargin = 0; detailLp.leftToRight = mAccessoryView.getId(); detailLp.rightMargin = accessoryLeftMargin; detailLp.goneRightMargin = 0; } } else { mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_title_h_text_size)); mDetailTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_detail_h_text_size)); titleLp.verticalChainStyle = LayoutParams.UNSET; titleLp.bottomToBottom = LayoutParams.PARENT_ID; titleLp.bottomToTop = LayoutParams.UNSET; detailLp.verticalChainStyle = LayoutParams.UNSET; detailLp.leftToLeft = LayoutParams.UNSET; detailLp.topToTop = LayoutParams.PARENT_ID; detailLp.topToBottom = LayoutParams.UNSET; detailLp.topMargin = 0; detailLp.leftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_detail_h_margin_with_title); if(mTipShown == TIP_SHOW_NEW){ if(mTipPosition == TIP_POSITION_LEFT){ updateTipLeftHorRelatedLayoutParam(mNewTipView, newTipLp, titleLp, detailLp); }else{ updateTipRightHorRelatedLayoutParam(mNewTipView, newTipLp, titleLp, detailLp); } }else if(mTipShown == TIP_SHOW_RED_POINT){ if(mTipPosition == TIP_POSITION_LEFT){ updateTipLeftHorRelatedLayoutParam(mRedDot, redDotLp, titleLp, detailLp); }else{ updateTipRightHorRelatedLayoutParam(mRedDot, redDotLp, titleLp, detailLp); } }else{ int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); titleLp.horizontalChainStyle = LayoutParams.UNSET; titleLp.rightToLeft = mAccessoryView.getId(); titleLp.rightMargin = accessoryLeftMargin; titleLp.goneRightMargin = 0; detailLp.leftToRight = mTextView.getId(); detailLp.rightToLeft = mAccessoryView.getId(); detailLp.rightMargin = accessoryLeftMargin; detailLp.goneRightMargin = 0; } } mTextView.setLayoutParams(titleLp); mDetailTextView.setLayoutParams(detailLp); mNewTipView.setLayoutParams(newTipLp); mRedDot.setLayoutParams(redDotLp); } private void updateTipLeftVerRelatedLayoutParam(View tipView, ConstraintLayout.LayoutParams tipLp, ConstraintLayout.LayoutParams titleLp, ConstraintLayout.LayoutParams detailLp){ int titleRightMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_holder_margin_with_title); int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); titleLp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; titleLp.horizontalBias = 0f; titleLp.rightToLeft = tipView.getId(); titleLp.rightMargin = titleRightMargin; tipLp.leftToRight = mTextView.getId(); tipLp.rightToLeft = mAccessoryView.getId(); tipLp.rightMargin = accessoryLeftMargin; tipLp.topToTop = mTextView.getId(); tipLp.bottomToBottom = mTextView.getId(); tipLp.goneRightMargin = 0; detailLp.rightToLeft = mAccessoryView.getId(); detailLp.rightMargin = accessoryLeftMargin; detailLp.goneRightMargin = 0; } private void updateTipRightVerRelatedLayoutParam(View tipView, ConstraintLayout.LayoutParams tipLp, ConstraintLayout.LayoutParams titleLp, ConstraintLayout.LayoutParams detailLp){ int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); tipLp.leftToRight = LayoutParams.UNSET; tipLp.rightToLeft = mAccessoryView.getId(); tipLp.rightMargin = accessoryLeftMargin; tipLp.goneRightMargin = 0; tipLp.topToTop = LayoutParams.PARENT_ID; tipLp.bottomToBottom = LayoutParams.PARENT_ID; titleLp.horizontalChainStyle = LayoutParams.UNSET; titleLp.rightToLeft = tipView.getId(); titleLp.rightMargin = accessoryLeftMargin; detailLp.rightToLeft = tipView.getId(); detailLp.rightMargin = accessoryLeftMargin; } private void updateTipLeftHorRelatedLayoutParam(View tipView, ConstraintLayout.LayoutParams tipLp, ConstraintLayout.LayoutParams titleLp, ConstraintLayout.LayoutParams detailLp){ int titleRightMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_holder_margin_with_title); int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); titleLp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; titleLp.horizontalBias = 0f; titleLp.rightToLeft = tipView.getId(); titleLp.rightMargin = titleRightMargin; tipLp.leftToRight = mTextView.getId(); tipLp.rightToLeft = mAccessoryView.getId(); tipLp.rightMargin = accessoryLeftMargin; tipLp.topToTop = mTextView.getId(); tipLp.bottomToBottom = mTextView.getId(); tipLp.goneRightMargin = 0; detailLp.leftToRight = tipView.getId(); detailLp.rightToLeft = mAccessoryView.getId(); detailLp.rightMargin = accessoryLeftMargin; detailLp.goneRightMargin = 0; } private void updateTipRightHorRelatedLayoutParam(View tipView, ConstraintLayout.LayoutParams tipLp, ConstraintLayout.LayoutParams titleLp, ConstraintLayout.LayoutParams detailLp){ int accessoryLeftMargin = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_common_list_item_accessory_margin_left); tipLp.leftToRight = LayoutParams.UNSET; tipLp.rightToLeft = mAccessoryView.getId(); tipLp.rightMargin = accessoryLeftMargin; tipLp.goneRightMargin = 0; tipLp.topToTop = LayoutParams.PARENT_ID; tipLp.bottomToBottom = LayoutParams.PARENT_ID; titleLp.horizontalChainStyle = LayoutParams.UNSET; titleLp.rightToLeft = tipView.getId(); titleLp.rightMargin = accessoryLeftMargin; titleLp.horizontalBias = 0f; detailLp.leftToRight = mTextView.getId(); detailLp.rightToLeft = tipView.getId(); detailLp.rightMargin = accessoryLeftMargin; } public int getAccessoryType() { return mAccessoryType; } /** * 设置右侧 View 的类型。 * * @param type 见 {@link QMUICommonListItemAccessoryType} */ public void setAccessoryType(@QMUICommonListItemAccessoryType int type) { mAccessoryView.removeAllViews(); mAccessoryType = type; switch (type) { // 向右的箭头 case ACCESSORY_TYPE_CHEVRON: { ImageView tempImageView = getAccessoryImageView(); tempImageView.setImageDrawable(QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_common_list_item_chevron)); mAccessoryView.addView(tempImageView); mAccessoryView.setVisibility(VISIBLE); } break; // switch开关 case ACCESSORY_TYPE_SWITCH: { if (mSwitch == null) { mSwitch = new AppCompatCheckBox(getContext()); mSwitch.setBackground(null); mSwitch.setButtonDrawable(QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_common_list_item_switch)); mSwitch.setLayoutParams(getAccessoryLayoutParams()); if(mDisableSwitchSelf){ mSwitch.setClickable(false); mSwitch.setEnabled(false); } } mAccessoryView.addView(mSwitch); mAccessoryView.setVisibility(VISIBLE); } break; // 自定义View case ACCESSORY_TYPE_CUSTOM: mAccessoryView.setVisibility(VISIBLE); break; // 清空所有accessoryView case ACCESSORY_TYPE_NONE: mAccessoryView.setVisibility(GONE); break; } LayoutParams titleLp = (LayoutParams) mTextView.getLayoutParams(); LayoutParams detailLp = (LayoutParams) mDetailTextView.getLayoutParams(); if (mAccessoryView.getVisibility() != View.GONE) { detailLp.goneRightMargin = detailLp.rightMargin; titleLp.goneRightMargin = titleLp.rightMargin; } else { detailLp.goneRightMargin = 0; titleLp.goneRightMargin = 0; } } private ViewGroup.LayoutParams getAccessoryLayoutParams() { return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } private ImageView getAccessoryImageView() { AppCompatImageView resultImageView = new AppCompatImageView(getContext()); resultImageView.setLayoutParams(getAccessoryLayoutParams()); resultImageView.setScaleType(ImageView.ScaleType.CENTER); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.tintColor(R.attr.qmui_skin_support_common_list_chevron_color); QMUISkinHelper.setSkinValue(resultImageView, builder); QMUISkinValueBuilder.release(builder); return resultImageView; } public TextView getTextView() { return mTextView; } public TextView getDetailTextView() { return mDetailTextView; } public CheckBox getSwitch() { return mSwitch; } public ViewGroup getAccessoryContainerView() { return mAccessoryView; } /** * 添加自定义的 Accessory View * * @param view 自定义的 Accessory View */ public void addAccessoryCustomView(View view) { if (mAccessoryType == ACCESSORY_TYPE_CUSTOM) { mAccessoryView.addView(view); } } public void setDisableSwitchSelf(boolean disableSwitchSelf) { mDisableSwitchSelf = disableSwitchSelf; if(mSwitch != null){ mSwitch.setClickable(!disableSwitchSelf); mSwitch.setEnabled(!disableSwitchSelf); } } public void setSkinConfig(SkinConfig skinConfig) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (skinConfig.iconTintColorRes != 0) { builder.tintColor(skinConfig.iconTintColorRes); } if (skinConfig.iconSrcRes != 0) { builder.src(skinConfig.iconSrcRes); } QMUISkinHelper.setSkinValue(mImageView, builder); builder.clear(); if (skinConfig.titleTextColorRes != 0) { builder.textColor(skinConfig.titleTextColorRes); } QMUISkinHelper.setSkinValue(mTextView, builder); builder.clear(); if (skinConfig.detailTextColorRes != 0) { builder.textColor(skinConfig.detailTextColorRes); } QMUISkinHelper.setSkinValue(mDetailTextView, builder); builder.clear(); if (skinConfig.newTipSrcRes != 0) { builder.src(skinConfig.newTipSrcRes); } QMUISkinHelper.setSkinValue(mNewTipView, builder); builder.clear(); if (skinConfig.tipDotColorRes != 0) { builder.bgTintColor(skinConfig.tipDotColorRes); } QMUISkinHelper.setSkinValue(mRedDot, builder); builder.release(); } public interface LayoutParamConfig { ConstraintLayout.LayoutParams onConfig(ConstraintLayout.LayoutParams lp); } public static class SkinConfig { public int iconTintColorRes = R.attr.qmui_skin_support_common_list_icon_tint_color; public int iconSrcRes = 0; public int titleTextColorRes = R.attr.qmui_skin_support_common_list_title_color; public int detailTextColorRes = R.attr.qmui_skin_support_common_list_detail_color; public int newTipSrcRes = R.attr.qmui_skin_support_common_list_new_drawable; public int tipDotColorRes = R.attr.qmui_skin_support_common_list_red_point_tint_color; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUIGroupListSectionHeaderFooterView.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.widget.grouplist; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.TextView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUILangHelper; /** * 用作通用列表 {@link QMUIGroupListView} 里每个 {@link QMUIGroupListView.Section} 的头部或尾部,也可单独使用。 * * @author molicechen * @date 2015-01-07 */ public class QMUIGroupListSectionHeaderFooterView extends LinearLayout { private TextView mTextView; public QMUIGroupListSectionHeaderFooterView(Context context) { this(context, null, R.attr.QMUIGroupListSectionViewStyle); } public QMUIGroupListSectionHeaderFooterView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUIGroupListSectionViewStyle); } public QMUIGroupListSectionHeaderFooterView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } public QMUIGroupListSectionHeaderFooterView(Context context, CharSequence titleText) { this(context); setText(titleText); } public QMUIGroupListSectionHeaderFooterView(Context context, CharSequence titleText, boolean isFooter) { this(context); if (isFooter) { // Footer View 不需要 padding bottom setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), 0); } setText(titleText); } private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.qmui_group_list_section_layout, this, true); setGravity(Gravity.BOTTOM); mTextView = (TextView) findViewById(R.id.group_list_section_header_textView); } public void setText(CharSequence text) { if (QMUILangHelper.isNullOrEmpty(text)) { mTextView.setVisibility(GONE); } else { mTextView.setVisibility(VISIBLE); } mTextView.setText(text); } public TextView getTextView() { return mTextView; } public void setTextGravity(int gravity) { mTextView.setGravity(gravity); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/grouplist/QMUIGroupListView.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.widget.grouplist; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.SparseArray; import android.view.ViewGroup; import android.widget.LinearLayout; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; /** * 通用的列表, 常用于 App 的设置界面。 *

* 注意其父类不是 {@link android.widget.ListView}, 而是 {@link LinearLayout}, 一般需要在外层包多一个 {@link android.widget.ScrollView} 来支持滚动。 *

*

* 提供了 {@link Section} 的概念, 用来将列表分块。 具体见 {@link QMUIGroupListView.Section} *

* * @author cginechen * @date 2016-10-13 */ public class QMUIGroupListView extends LinearLayout { private SparseArray
mSections; public QMUIGroupListView(Context context) { this(context, null); } public QMUIGroupListView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIGroupListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mSections = new SparseArray<>(); setOrientation(LinearLayout.VERTICAL); } /** * 创建一个 Section。 * * @return 返回新创建的 Section。 */ public static Section newSection(Context context) { return new Section(context); } public int getSectionCount() { return mSections.size(); } public QMUICommonListItemView createItemView(@Nullable Drawable imageDrawable, CharSequence titleText, String detailText, int orientation, int accessoryType, int height) { QMUICommonListItemView itemView = new QMUICommonListItemView(getContext()); itemView.setOrientation(orientation); itemView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height)); itemView.setImageDrawable(imageDrawable); itemView.setText(titleText); itemView.setDetailText(detailText); itemView.setAccessoryType(accessoryType); return itemView; } public QMUICommonListItemView createItemView(@Nullable Drawable imageDrawable, CharSequence titleText, String detailText, int orientation, int accessoryType) { int height; if (orientation == QMUICommonListItemView.VERTICAL) { height = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_list_item_height_higher); return createItemView(imageDrawable, titleText, detailText, orientation, accessoryType, height); } else { height = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_list_item_height); return createItemView(imageDrawable, titleText, detailText, orientation, accessoryType, height); } } public QMUICommonListItemView createItemView(CharSequence titleText) { return createItemView(null, titleText, null, QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_NONE); } public QMUICommonListItemView createItemView(int orientation) { return createItemView(null, null, null, orientation, QMUICommonListItemView.ACCESSORY_TYPE_NONE); } /** * private, use {@link Section#addTo(QMUIGroupListView)} *

这里只是把section记录到数组里面而已

*/ private void addSection(Section section) { mSections.append(mSections.size(), section); } /** * private,use {@link Section#removeFrom(QMUIGroupListView)} *

这里只是把section从记录的数组里移除而已

*/ private void removeSection(Section section) { for (int i = 0; i < mSections.size(); i++) { Section each = mSections.valueAt(i); if (each == section) { mSections.remove(i); } } } public Section getSection(int index) { return mSections.get(index); } /** * Section 是组成 {@link QMUIGroupListView} 的部分。 *
    *
  • 每个 Section 可以有多个 item, 通过 {@link #addItemView(QMUICommonListItemView, OnClickListener)} 添加。
  • *
  • Section 还可以有自己的一个顶部 title 和一个底部 description, 通过 {@link #setTitle(CharSequence)} 和 {@link #setDescription(CharSequence)} 设置。
  • *
*/ public static class Section { private Context mContext; private QMUIGroupListSectionHeaderFooterView mTitleView; private QMUIGroupListSectionHeaderFooterView mDescriptionView; private SparseArray mItemViews; private boolean mUseDefaultTitleIfNone; private boolean mUseTitleViewForSectionSpace = true; private int mSeparatorColorAttr = R.attr.qmui_skin_support_common_list_separator_color; private boolean mHandleSeparatorCustom = false; private boolean mShowSeparator = true; private boolean mOnlyShowStartEndSeparator = false; private boolean mOnlyShowMiddleSeparator = false; private int mMiddleSeparatorInsetLeft = 0; private int mMiddleSeparatorInsetRight = 0; private int mBgAttr = R.attr.qmui_skin_support_s_common_list_bg; private int mLeftIconWidth = ViewGroup.LayoutParams.WRAP_CONTENT; private int mLeftIconHeight = ViewGroup.LayoutParams.WRAP_CONTENT; public Section(Context context) { mContext = context; mItemViews = new SparseArray<>(); } /** * 对 Section 添加一个 {@link QMUICommonListItemView} * * @param itemView 要添加的 ItemView * @param onClickListener ItemView 的点击事件 * @return Section 本身,支持链式调用 */ public Section addItemView(QMUICommonListItemView itemView, OnClickListener onClickListener) { return addItemView(itemView, onClickListener, null); } /** * 对 Section 添加一个 {@link QMUICommonListItemView} * * @param itemView 要添加的 ItemView * @param onClickListener ItemView 的点击事件 * @param onLongClickListener ItemView 的长按事件 * @return Section 本身, 支持链式调用 */ public Section addItemView(final QMUICommonListItemView itemView, OnClickListener onClickListener, OnLongClickListener onLongClickListener) { if (onClickListener != null) { itemView.setOnClickListener(onClickListener); } if (onLongClickListener != null) { itemView.setOnLongClickListener(onLongClickListener); } mItemViews.append(mItemViews.size(), itemView); return this; } /** * 设置 Section 的 title * * @return Section 本身, 支持链式调用 */ public Section setTitle(CharSequence title) { mTitleView = createSectionHeader(title); return this; } /** * 设置 Section 的 description * * @return Section 本身, 支持链式调用 */ public Section setDescription(CharSequence description) { mDescriptionView = createSectionFooter(description); return this; } public Section setUseDefaultTitleIfNone(boolean useDefaultTitleIfNone) { mUseDefaultTitleIfNone = useDefaultTitleIfNone; return this; } public Section setUseTitleViewForSectionSpace(boolean useTitleViewForSectionSpace) { mUseTitleViewForSectionSpace = useTitleViewForSectionSpace; return this; } public Section setLeftIconSize(int width, int height) { mLeftIconHeight = height; mLeftIconWidth = width; return this; } public Section setSeparatorColorAttr(int attr) { mSeparatorColorAttr = attr; return this; } public Section setHandleSeparatorCustom(boolean handleSeparatorCustom) { mHandleSeparatorCustom = handleSeparatorCustom; return this; } public Section setShowSeparator(boolean showSeparator) { mShowSeparator = showSeparator; return this; } public Section setOnlyShowStartEndSeparator(boolean onlyShowStartEndSeparator) { mOnlyShowStartEndSeparator = onlyShowStartEndSeparator; return this; } public Section setOnlyShowMiddleSeparator(boolean onlyShowMiddleSeparator) { mOnlyShowMiddleSeparator = onlyShowMiddleSeparator; return this; } public Section setMiddleSeparatorInset(int insetLeft, int insetRight) { mMiddleSeparatorInsetLeft = insetLeft; mMiddleSeparatorInsetRight = insetRight; return this; } public Section setBgAttr(int bgAttr) { mBgAttr = bgAttr; return this; } /** * 将 Section 添加到 {@link QMUIGroupListView} 上 */ public void addTo(QMUIGroupListView groupListView) { if (mTitleView == null) { if (mUseDefaultTitleIfNone) { setTitle("Section " + groupListView.getSectionCount()); } else if (mUseTitleViewForSectionSpace) { setTitle(""); } } if (mTitleView != null) { groupListView.addView(mTitleView); } final int itemViewCount = mItemViews.size(); QMUICommonListItemView.LayoutParamConfig leftIconLpConfig = new QMUICommonListItemView.LayoutParamConfig() { @Override public ConstraintLayout.LayoutParams onConfig(ConstraintLayout.LayoutParams lp) { lp.width = mLeftIconWidth; lp.height = mLeftIconHeight; return lp; } }; QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); String skin = builder.background(mBgAttr) .topSeparator(mSeparatorColorAttr) .bottomSeparator(mSeparatorColorAttr) .build(); QMUISkinValueBuilder.release(builder); int separatorColor = QMUIResHelper.getAttrColor(groupListView.getContext(), mSeparatorColorAttr); for (int i = 0; i < itemViewCount; i++) { QMUICommonListItemView itemView = mItemViews.get(i); Drawable bg = QMUISkinHelper.getSkinDrawable(groupListView, mBgAttr); QMUIViewHelper.setBackgroundKeepingPadding(itemView, bg == null ? null : bg.mutate()); QMUISkinHelper.setSkinValue(itemView, skin); if (!mHandleSeparatorCustom && mShowSeparator) { if (itemViewCount == 1) { itemView.updateTopDivider(0, 0, 1, separatorColor); itemView.updateBottomDivider(0, 0, 1, separatorColor); } else if (i == 0) { if(!mOnlyShowMiddleSeparator){ itemView.updateTopDivider(0, 0, 1, separatorColor); } if (!mOnlyShowStartEndSeparator) { itemView.updateBottomDivider( mMiddleSeparatorInsetLeft, mMiddleSeparatorInsetRight, 1, separatorColor); } } else if (i == itemViewCount - 1) { if(!mOnlyShowMiddleSeparator){ itemView.updateBottomDivider(0, 0, 1, separatorColor); } } else if (!mOnlyShowStartEndSeparator) { itemView.updateBottomDivider(mMiddleSeparatorInsetLeft, mMiddleSeparatorInsetRight, 1, separatorColor); } } itemView.updateImageViewLp(leftIconLpConfig); groupListView.addView(itemView); } if (mDescriptionView != null) { groupListView.addView(mDescriptionView); } groupListView.addSection(this); } public void removeFrom(QMUIGroupListView parent) { if (mTitleView != null && mTitleView.getParent() == parent) { parent.removeView(mTitleView); } for (int i = 0; i < mItemViews.size(); i++) { QMUICommonListItemView itemView = mItemViews.get(i); parent.removeView(itemView); } if (mDescriptionView != null && mDescriptionView.getParent() == parent) { parent.removeView(mDescriptionView); } parent.removeSection(this); } /** * 创建 Section Header,每个 Section 都会被创建一个 Header,有 title 时会显示 title,没有 title 时会利用 header 的上下 padding 充当 Section 分隔条 */ public QMUIGroupListSectionHeaderFooterView createSectionHeader(CharSequence titleText) { return new QMUIGroupListSectionHeaderFooterView(mContext, titleText); } /** * Section 的 Footer,形式与 Header 相似,都是显示一段文字 */ public QMUIGroupListSectionHeaderFooterView createSectionFooter(CharSequence text) { return new QMUIGroupListSectionHeaderFooterView(mContext, text, true); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIBasePopup.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.widget.popup; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.PopupWindow; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import java.lang.ref.WeakReference; public abstract class QMUIBasePopup { public static final float DIM_AMOUNT_NOT_EXIST = -1f; public static final int NOT_SET = -1; protected final PopupWindow mWindow; protected WindowManager mWindowManager; protected Context mContext; protected WeakReference mAttachedViewRf; private float mDimAmount = DIM_AMOUNT_NOT_EXIST; private int mDimAmountAttr = 0; private PopupWindow.OnDismissListener mDismissListener; private QMUISkinManager mSkinManager; private QMUISkinManager.OnSkinChangeListener mOnSkinChangeListener = new QMUISkinManager.OnSkinChangeListener() { @Override public void onSkinChange(QMUISkinManager skinManager, int oldSkin, int newSkin) { if (mDimAmountAttr != 0) { Resources.Theme theme = skinManager.getTheme(newSkin); mDimAmount = QMUIResHelper.getAttrFloatValue(theme, mDimAmountAttr); updateDimAmount(mDimAmount); QMUIBasePopup.this.onSkinChange(oldSkin, newSkin); } } }; private View.OnAttachStateChangeListener mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { dismiss(); } }; private View.OnTouchListener mOutsideTouchDismissListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { mWindow.dismiss(); return true; } return false; } }; public QMUIBasePopup(Context context) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mWindow = new PopupWindow(context); mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); mWindow.setFocusable(true); mWindow.setTouchable(true); mWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { removeOldAttachStateChangeListener(); mAttachedViewRf = null; if(mSkinManager != null){ mSkinManager.unRegister(mWindow); mSkinManager.removeSkinChangeListener(mOnSkinChangeListener); } QMUIBasePopup.this.onDismiss(); if (mDismissListener != null) { mDismissListener.onDismiss(); } } }); dismissIfOutsideTouch(true); } protected void onSkinChange(int oldSkin, int newSkin){ } public QMUISkinManager getSkinManager() { return mSkinManager; } public T dimAmount(float dimAmount) { mDimAmount = dimAmount; return (T) this; } public T dimAmountAttr(int dimAmountAttr) { mDimAmountAttr = dimAmountAttr; return (T) this; } public T skinManager(@Nullable QMUISkinManager skinManager) { mSkinManager = skinManager; return (T) this; } public T setTouchable(boolean touchable){ mWindow.setTouchable(true); return (T) this; } public T setFocusable(boolean focusable){ mWindow.setFocusable(focusable); return (T) this; } public T dismissIfOutsideTouch(boolean dismissIfOutsideTouch) { mWindow.setOutsideTouchable(dismissIfOutsideTouch); if (dismissIfOutsideTouch) { mWindow.setTouchInterceptor(mOutsideTouchDismissListener); } else { mWindow.setTouchInterceptor(null); } return (T) this; } public T onDismiss(PopupWindow.OnDismissListener listener) { mDismissListener = listener; return (T) this; } private void removeOldAttachStateChangeListener() { if (mAttachedViewRf != null) { View oldAttachedView = mAttachedViewRf.get(); if (oldAttachedView != null) { oldAttachedView.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); } } } public View getDecorView() { View decorView = null; try { if (mWindow.getBackground() == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { decorView = (View) mWindow.getContentView().getParent(); } else { decorView = mWindow.getContentView(); } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { decorView = (View) mWindow.getContentView().getParent().getParent(); } else { decorView = (View) mWindow.getContentView().getParent(); } } } catch (Exception ignore) { } return decorView; } protected void showAtLocation(@NonNull View parent, int x, int y) { if (!ViewCompat.isAttachedToWindow(parent)) { return; } removeOldAttachStateChangeListener(); parent.addOnAttachStateChangeListener(mOnAttachStateChangeListener); mAttachedViewRf = new WeakReference<>(parent); mWindow.showAtLocation(parent, Gravity.NO_GRAVITY, x, y); if (mSkinManager != null) { mSkinManager.register(mWindow); mSkinManager.addSkinChangeListener(mOnSkinChangeListener); if (mDimAmountAttr != 0) { Resources.Theme currentTheme = mSkinManager.getCurrentTheme(); currentTheme = currentTheme == null ? parent.getContext().getTheme() : currentTheme; mDimAmount = QMUIResHelper.getAttrFloatValue(currentTheme, mDimAmountAttr); } } if (mDimAmount != DIM_AMOUNT_NOT_EXIST) { updateDimAmount(mDimAmount); } } private void updateDimAmount(float dimAmount) { View decorView = getDecorView(); if (decorView != null) { WindowManager.LayoutParams p = (WindowManager.LayoutParams) decorView.getLayoutParams(); p.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; p.dimAmount = dimAmount; modifyWindowLayoutParams(p); mWindowManager.updateViewLayout(decorView, p); } } protected void modifyWindowLayoutParams(WindowManager.LayoutParams lp) { } protected void onDismiss() { } public final void dismiss() { mWindow.dismiss(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIFullScreenPopup.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.widget.popup; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaImageButton; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.IBlankTouchDetector; import java.util.ArrayList; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; public class QMUIFullScreenPopup extends QMUIBasePopup { private OnBlankClickListener mOnBlankClickListener; private boolean mAddCloseBtn = false; private int mCloseIconAttr = R.attr.qmui_skin_support_popup_close_icon; private Drawable mCloseIcon = null; private ConstraintLayout.LayoutParams mCloseIvLayoutParams; private int mAnimStyle = NOT_SET; private ArrayList mViews = new ArrayList<>(); public QMUIFullScreenPopup(Context context) { super(context); mWindow.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); mWindow.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); dimAmount(0.6f); } public QMUIFullScreenPopup onBlankClick(OnBlankClickListener onBlankClickListener) { mOnBlankClickListener = onBlankClickListener; return this; } public QMUIFullScreenPopup closeBtn(boolean close) { mAddCloseBtn = close; return this; } public QMUIFullScreenPopup closeIcon(Drawable drawable) { mCloseIcon = drawable; return this; } public QMUIFullScreenPopup closeIconAttr(int closeIconAttr) { mCloseIconAttr = closeIconAttr; return this; } public QMUIFullScreenPopup closeLp(ConstraintLayout.LayoutParams contentLayoutParams) { mCloseIvLayoutParams = contentLayoutParams; return this; } public int getCloseBtnId() { return R.id.qmui_popup_close_btn_id; } public QMUIFullScreenPopup animStyle(int animStyle) { mAnimStyle = animStyle; return this; } public QMUIFullScreenPopup addView(View view, ConstraintLayout.LayoutParams lp) { mViews.add(new ViewInfo(view, lp)); return this; } public QMUIFullScreenPopup addView(View view) { mViews.add(new ViewInfo(view, defaultContentLp())); return this; } private ConstraintLayout.LayoutParams defaultContentLp() { ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); lp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; lp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; lp.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; return lp; } private ConstraintLayout.LayoutParams defaultCloseIvLp() { ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); lp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; lp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; lp.bottomMargin = QMUIDisplayHelper.dp2px(mContext, 48); return lp; } private QMUIAlphaImageButton createCloseIv() { QMUIAlphaImageButton closeBtn = new QMUIAlphaImageButton(mContext); closeBtn.setPadding(0, 0, 0, 0); closeBtn.setScaleType(ImageView.ScaleType.CENTER); closeBtn.setId(R.id.qmui_popup_close_btn_id); closeBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); } }); closeBtn.setFitsSystemWindows(true); Drawable drawable = null; if (mCloseIcon != null) { drawable = mCloseIcon; } else if (mCloseIconAttr != 0) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire().src(mCloseIconAttr); QMUISkinHelper.setSkinValue(closeBtn, builder); builder.release(); drawable = QMUIResHelper.getAttrDrawable(mContext, mCloseIconAttr); } closeBtn.setImageDrawable(drawable); return closeBtn; } public boolean isShowing() { return mWindow.isShowing(); } public void show(View parent) { if (isShowing()) { return; } if (mViews.isEmpty()) { throw new RuntimeException("you should call addView() to add content view"); } ArrayList views = new ArrayList<>(mViews); RootView rootView = new RootView(mContext); for (int i = 0; i < views.size(); i++) { ViewInfo info = mViews.get(i); View view = info.view; if (view.getParent() != null) { ((ViewGroup) view.getParent()).removeView(view); } rootView.addView(view, info.lp); } if (mAddCloseBtn) { if (mCloseIvLayoutParams == null) { mCloseIvLayoutParams = defaultCloseIvLp(); } rootView.addView(createCloseIv(), mCloseIvLayoutParams); } mWindow.setContentView(rootView); if (mAnimStyle != NOT_SET) { mWindow.setAnimationStyle(mAnimStyle); } showAtLocation(parent, 0, 0); } @Override protected void modifyWindowLayoutParams(WindowManager.LayoutParams lp) { lp.flags |= FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR; super.modifyWindowLayoutParams(lp); } public interface OnBlankClickListener { void onBlankClick(QMUIFullScreenPopup popup); } class RootView extends QMUIConstraintLayout { private boolean mShouldInvokeBlackClickWhenTouchUp = false; public RootView(Context context) { super(context); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if (mOnBlankClickListener == null) { return true; } if (action == MotionEvent.ACTION_DOWN) { mShouldInvokeBlackClickWhenTouchUp = isTouchInBlack(event); } else if (action == MotionEvent.ACTION_MOVE) { mShouldInvokeBlackClickWhenTouchUp = mShouldInvokeBlackClickWhenTouchUp && isTouchInBlack(event); } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mShouldInvokeBlackClickWhenTouchUp = mShouldInvokeBlackClickWhenTouchUp && isTouchInBlack(event); if (mShouldInvokeBlackClickWhenTouchUp) { mOnBlankClickListener.onBlankClick(QMUIFullScreenPopup.this); } } return true; } private boolean isTouchInBlack(MotionEvent event) { View childView = findChildViewUnder(event.getX(), event.getY()); boolean isBlank = childView == null; if (!isBlank && (childView instanceof IBlankTouchDetector)) { MotionEvent e = MotionEvent.obtain(event); int offsetX = getScrollX() - childView.getLeft(); int offsetY = getScrollY() - childView.getTop(); e.offsetLocation(offsetX, offsetY); isBlank = ((IBlankTouchDetector) childView).isTouchInBlank(e); e.recycle(); } return isBlank; } private View findChildViewUnder(float x, float y) { final int count = getChildCount(); for (int i = count - 1; i >= 0; i--) { final View child = getChildAt(i); final float translationX = child.getTranslationX(); final float translationY = child.getTranslationY(); if (x >= child.getLeft() + translationX && x <= child.getRight() + translationX && y >= child.getTop() + translationY && y <= child.getBottom() + translationY) { return child; } } return null; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); for (ViewInfo viewInfo : mViews) { View view = viewInfo.view; QMUIViewHelper.getOrCreateOffsetHelper(view).onViewLayout(); } } } class ViewInfo { private View view; private ConstraintLayout.LayoutParams lp; public ViewInfo(View view, ConstraintLayout.LayoutParams lp) { this.view = view; this.lp = lp; } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUINormalPopup.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.widget.popup; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; import androidx.annotation.AnimRes; import androidx.annotation.IntDef; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.layout.QMUILayoutHelper; import com.qmuiteam.qmui.skin.IQMUISkinDispatchInterceptor; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class QMUINormalPopup extends QMUIBasePopup { public static final int ANIM_AUTO = 0; public static final int ANIM_GROW_FROM_LEFT = 1; public static final int ANIM_GROW_FROM_RIGHT = 2; public static final int ANIM_GROW_FROM_CENTER = 3; public static final int ANIM_SPEC = 4; @IntDef(value = {ANIM_AUTO, ANIM_GROW_FROM_LEFT, ANIM_GROW_FROM_RIGHT, ANIM_GROW_FROM_CENTER, ANIM_SPEC}) public @interface AnimStyle { } public static final int DIRECTION_TOP = 0; public static final int DIRECTION_BOTTOM = 1; public static final int DIRECTION_CENTER_IN_SCREEN = 2; @IntDef({DIRECTION_CENTER_IN_SCREEN, DIRECTION_TOP, DIRECTION_BOTTOM}) @Retention(RetentionPolicy.SOURCE) public @interface Direction { } protected @AnimStyle int mAnimStyle; protected int mSpecAnimStyle; private int mEdgeProtectionTop; private int mEdgeProtectionLeft; private int mEdgeProtectionRight; private int mEdgeProtectionBottom; private boolean mShowArrow = true; private boolean mAddShadow = false; private int mRadius = NOT_SET; private int mBorderColor = Color.TRANSPARENT; private int mBorderUsedColor = Color.TRANSPARENT; private int mBorderColorAttr = R.attr.qmui_skin_support_popup_border_color; private boolean mIsBorderColorSet = false; private int mBorderWidth = NOT_SET; private int mShadowElevation = NOT_SET; private float mShadowAlpha = 0f; private int mShadowInset = NOT_SET; private int mBgColor = Color.TRANSPARENT; private boolean mIsBgColorSet= false; private int mBgUsedColor = Color.TRANSPARENT; private int mBgColorAttr = R.attr.qmui_skin_support_popup_bg; private int mOffsetX = 0; private int mOffsetYIfTop = 0; private int mOffsetYIfBottom = 0; private @Direction int mPreferredDirection = DIRECTION_BOTTOM; protected final int mInitWidth; protected final int mInitHeight; private int mArrowWidth = NOT_SET; private int mArrowHeight = NOT_SET; private boolean mRemoveBorderWhenShadow = false; private DecorRootView mDecorRootView; private View mContentView; private boolean mForceMeasureIfNeeded; public QMUINormalPopup(Context context, int width, int height){ this(context, width, height, true); } public QMUINormalPopup(Context context, int width, int height, boolean forceMeasureIfNeeded) { super(context); mInitWidth = width; mInitHeight = height; mDecorRootView = new DecorRootView(context); mWindow.setContentView(mDecorRootView); mForceMeasureIfNeeded = forceMeasureIfNeeded; } public T arrow(boolean showArrow) { mShowArrow = showArrow; return (T) this; } public T arrowSize(int width, int height) { mArrowWidth = width; mArrowHeight = height; return (T) this; } public T shadow(boolean addShadow) { mAddShadow = addShadow; return (T) this; } public T removeBorderWhenShadow(boolean removeBorderWhenShadow) { mRemoveBorderWhenShadow = removeBorderWhenShadow; return (T) this; } public T animStyle(@AnimStyle int animStyle) { mAnimStyle = animStyle; return (T) this; } public T customAnimStyle(@AnimRes int animStyle) { mAnimStyle = ANIM_SPEC; mSpecAnimStyle = animStyle; return (T) this; } public T radius(int radius) { mRadius = radius; return (T) this; } public T shadowElevation(int shadowElevation, float shadowAlpha) { mShadowAlpha = shadowAlpha; mShadowElevation = shadowElevation; return (T) this; } public T shadowInset(int shadowInset) { mShadowInset = shadowInset; return (T) this; } public T edgeProtection(int distance) { mEdgeProtectionLeft = distance; mEdgeProtectionRight = distance; mEdgeProtectionTop = distance; mEdgeProtectionBottom = distance; return (T) this; } public T edgeProtection(int left, int top, int right, int bottom) { mEdgeProtectionLeft = left; mEdgeProtectionTop = top; mEdgeProtectionRight = right; mEdgeProtectionBottom = bottom; return (T) this; } public T offsetX(int offsetX) { mOffsetX = offsetX; return (T) this; } public T offsetYIfTop(int y) { mOffsetYIfTop = y; return (T) this; } public T offsetYIfBottom(int y) { mOffsetYIfBottom = y; return (T) this; } public T preferredDirection(@Direction int preferredDirection) { mPreferredDirection = preferredDirection; return (T) this; } public T view(View contentView) { mContentView = contentView; return (T) this; } public T view(@LayoutRes int contentViewResId) { return view(LayoutInflater.from(mContext).inflate(contentViewResId, null)); } @NonNull public View getDecorRootView(){ return mDecorRootView; } public View getWindowContentChildView(){ View self = mDecorRootView; ViewParent parent = mDecorRootView.getParent(); while (parent instanceof View){ if(((View) parent).getId() == android.R.id.content){ return self; } self = (View)parent; parent = self.getParent(); } return self; } @Nullable public View getContentView(){ return mContentView; } public T borderWidth(int borderWidth) { mBorderWidth = borderWidth; return (T) this; } public T borderColor(int borderColor) { mBorderColor = borderColor; mIsBorderColorSet = true; return (T) this; } public int getBgColor() { return mBgColor; } public int getBgColorAttr() { return mBgColorAttr; } public int getBorderColor() { return mBorderColor; } public int getBorderColorAttr() { return mBorderColorAttr; } public T bgColor(int bgColor) { mBgColor = bgColor; mIsBgColorSet = true; return (T) this; } public T borderColorAttr(int borderColorAttr) { mBorderColorAttr = borderColorAttr; if(borderColorAttr != 0){ mIsBorderColorSet = false; } return (T) this; } public T bgColorAttr(int bgColorAttr) { mBgColorAttr = bgColorAttr; if(bgColorAttr != 0){ mIsBgColorSet = false; } return (T) this; } class ShowInfo { private int[] anchorRootLocation = new int[2]; private Rect anchorFrame = new Rect(); Rect visibleWindowFrame = new Rect(); int width; int height; int x; int y; int anchorHeight; int anchorCenter; int direction = mPreferredDirection; int contentWidthMeasureSpec; int contentHeightMeasureSpec; int decorationLeft = 0; int decorationRight = 0; int decorationTop = 0; int decorationBottom = 0; ShowInfo(View anchor, int anchorAreaLeft, int anchorAreaTop, int anchorAreaRight, int anchorAreaBottom) { this.anchorHeight = anchorAreaBottom - anchorAreaTop; // for muti window anchor.getRootView().getLocationOnScreen(anchorRootLocation); int[] anchorLocation = new int[2]; anchor.getLocationOnScreen(anchorLocation); this.anchorCenter = anchorLocation[0] + (anchorAreaLeft + anchorAreaRight) / 2; anchor.getWindowVisibleDisplayFrame(visibleWindowFrame); anchorFrame.left = anchorLocation [0] + anchorAreaLeft; anchorFrame.top = anchorLocation[1] + anchorAreaTop; anchorFrame.right = anchorLocation [0] + anchorAreaRight; anchorFrame.bottom = anchorLocation [1] + anchorAreaBottom; } ShowInfo(View anchor){ this(anchor, 0, 0, anchor.getWidth(), anchor.getHeight()); } float anchorProportion() { return (anchorCenter - x) / (float) width; } int windowWidth() { return decorationLeft + width + decorationRight; } int windowHeight() { return decorationTop + height + decorationBottom; } int getVisibleWidth() { return visibleWindowFrame.width(); } int getVisibleHeight() { return visibleWindowFrame.height(); } int getWindowX() { return x - anchorRootLocation[0]; } int getWindowY() { return y - anchorRootLocation[1]; } } private boolean shouldShowShadow() { return mAddShadow && QMUILayoutHelper.useFeature(); } public T show(@NonNull View anchor) { return show(anchor, 0, 0, anchor.getWidth(), anchor.getHeight()); } public T show(@NonNull View anchor, int anchorAreaLeft, int anchorAreaTop, int anchorAreaRight, int anchorAreaBottom){ if (mContentView == null) { throw new RuntimeException("you should call view() to set your content view"); } decorateContentView(); ShowInfo showInfo = new ShowInfo(anchor, anchorAreaLeft, anchorAreaTop, anchorAreaRight, anchorAreaBottom); calculateWindowSize(showInfo); calculateXY(showInfo); adjustShowInfo(showInfo); mDecorRootView.setShowInfo(showInfo); setAnimationStyle(showInfo.anchorProportion(), showInfo.direction); mWindow.setWidth(showInfo.windowWidth()); mWindow.setHeight(showInfo.windowHeight()); showAtLocation(anchor, showInfo.getWindowX(), showInfo.getWindowY()); return (T) this; } private void decorateContentView() { ContentView contentView = ContentView.wrap(mContentView, mInitWidth, mInitHeight); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (mIsBorderColorSet) { mBorderUsedColor = mBorderColor; } else if (mBorderColorAttr != 0) { mBorderUsedColor = QMUIResHelper.getAttrColor(mContext, mBorderColorAttr); builder.border(mBorderColorAttr); } if (mIsBgColorSet) { mBgUsedColor = mBgColor; } else if (mBgColorAttr != 0) { mBgUsedColor = QMUIResHelper.getAttrColor(mContext, mBgColorAttr); builder.background(mBgColorAttr); } if (mBorderWidth == NOT_SET) { mBorderWidth = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_border_width); } QMUISkinHelper.setSkinValue(contentView, builder); builder.release(); contentView.setBackgroundColor(mBgUsedColor); contentView.setBorderColor(mBorderUsedColor); contentView.setBorderWidth(mBorderWidth); contentView.setShowBorderOnlyBeforeL(mRemoveBorderWhenShadow); if (mRadius == NOT_SET) { mRadius = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_radius); } if (shouldShowShadow()) { contentView.setRadiusAndShadow(mRadius, mShadowElevation, mShadowAlpha); } else { contentView.setRadius(mRadius); } mDecorRootView.setContentView(contentView); } private void adjustShowInfo(ShowInfo showInfo) { if (shouldShowShadow()) { if (mShadowElevation == NOT_SET) { mShadowElevation = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_shadow_elevation); mShadowAlpha = QMUIResHelper.getAttrFloatValue(mContext, R.attr.qmui_popup_shadow_alpha); } if (mShadowInset == NOT_SET) { mShadowInset = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_shadow_inset); } int originX = showInfo.x, originY = showInfo.y; if (originX - mShadowInset > showInfo.visibleWindowFrame.left) { showInfo.x -= mShadowInset; showInfo.decorationLeft = mShadowInset; } else { showInfo.decorationLeft = originX - showInfo.visibleWindowFrame.left; showInfo.x = showInfo.visibleWindowFrame.left; } if (originX + showInfo.width + mShadowInset < showInfo.visibleWindowFrame.right) { showInfo.decorationRight = mShadowInset; } else { showInfo.decorationRight = showInfo.visibleWindowFrame.right - originX - showInfo.width; } if (originY - mShadowInset > showInfo.visibleWindowFrame.top) { showInfo.y -= mShadowInset; showInfo.decorationTop = mShadowInset; } else { showInfo.decorationTop = originY - showInfo.visibleWindowFrame.top; showInfo.y = showInfo.visibleWindowFrame.top; } if (originY + showInfo.height + mShadowInset < showInfo.visibleWindowFrame.bottom) { showInfo.decorationBottom = mShadowInset; } else { showInfo.decorationBottom = showInfo.visibleWindowFrame.bottom - originY - showInfo.height; } } if (mShowArrow && showInfo.direction != DIRECTION_CENTER_IN_SCREEN) { if (mArrowWidth == NOT_SET) { mArrowWidth = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_arrow_width); } if (mArrowHeight == NOT_SET) { mArrowHeight = QMUIResHelper.getAttrDimen(mContext, R.attr.qmui_popup_arrow_height); } if (showInfo.direction == DIRECTION_BOTTOM) { if (shouldShowShadow()) { showInfo.y += Math.min(mShadowInset, mArrowHeight); } showInfo.decorationTop = Math.max(showInfo.decorationTop, mArrowHeight); } else if (showInfo.direction == DIRECTION_TOP) { showInfo.decorationBottom = Math.max(showInfo.decorationBottom, mArrowHeight); showInfo.y -= mArrowHeight; } } } private void calculateXY(ShowInfo showInfo) { if (showInfo.anchorCenter < showInfo.visibleWindowFrame.left + showInfo.getVisibleWidth() / 2) { // anchor point on the left showInfo.x = Math.max(mEdgeProtectionLeft + showInfo.visibleWindowFrame.left, showInfo.anchorCenter - showInfo.width / 2 + mOffsetX); } else { // anchor point on the left showInfo.x = Math.min( showInfo.visibleWindowFrame.right - mEdgeProtectionRight - showInfo.width, showInfo.anchorCenter - showInfo.width / 2 + mOffsetX); } int nextDirection = DIRECTION_CENTER_IN_SCREEN; if (mPreferredDirection == DIRECTION_BOTTOM) { nextDirection = DIRECTION_TOP; } else if (mPreferredDirection == DIRECTION_TOP) { nextDirection = DIRECTION_BOTTOM; } handleDirection(showInfo, mPreferredDirection, nextDirection); } private void handleDirection(ShowInfo showInfo, int currentDirection, int nextDirection) { if (currentDirection == DIRECTION_CENTER_IN_SCREEN) { showInfo.x = showInfo.visibleWindowFrame.left + (showInfo.getVisibleWidth() - showInfo.width) / 2; showInfo.y = showInfo.visibleWindowFrame.top + (showInfo.getVisibleHeight() - showInfo.height) / 2; showInfo.direction = DIRECTION_CENTER_IN_SCREEN; } else if (currentDirection == DIRECTION_TOP) { showInfo.y = showInfo.anchorFrame.top - showInfo.height - mOffsetYIfTop; if (showInfo.y < mEdgeProtectionTop + showInfo.visibleWindowFrame.top) { handleDirection(showInfo, nextDirection, DIRECTION_CENTER_IN_SCREEN); } else { showInfo.direction = DIRECTION_TOP; } } else if (currentDirection == DIRECTION_BOTTOM) { showInfo.y = showInfo.anchorFrame.top + showInfo.anchorHeight + mOffsetYIfBottom; if (showInfo.y > showInfo.visibleWindowFrame.bottom - mEdgeProtectionBottom - showInfo.height) { handleDirection(showInfo, nextDirection, DIRECTION_CENTER_IN_SCREEN); } else { showInfo.direction = DIRECTION_BOTTOM; } } } protected int proxyWidth(int width) { return width; } protected int proxyHeight(int height) { return height; } private void calculateWindowSize(ShowInfo showInfo) { boolean needMeasureForWidth = false, needMeasureForHeight = false; if (mInitWidth > 0) { showInfo.width = proxyWidth(mInitWidth); showInfo.contentWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec( showInfo.width, View.MeasureSpec.EXACTLY); } else { int maxWidth = showInfo.getVisibleWidth() - mEdgeProtectionLeft - mEdgeProtectionRight; if (mInitWidth == ViewGroup.LayoutParams.MATCH_PARENT) { showInfo.width = proxyWidth(maxWidth); showInfo.contentWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec( showInfo.width, View.MeasureSpec.EXACTLY); } else { needMeasureForWidth = true; showInfo.contentWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec( proxyWidth(maxWidth), View.MeasureSpec.AT_MOST); } } if (mInitHeight > 0) { showInfo.height = proxyHeight(mInitHeight); showInfo.contentHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( showInfo.height, View.MeasureSpec.EXACTLY); } else { int maxHeight = showInfo.getVisibleHeight() - mEdgeProtectionTop - mEdgeProtectionBottom; if (mInitHeight == ViewGroup.LayoutParams.MATCH_PARENT) { showInfo.height = proxyHeight(maxHeight); showInfo.contentHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( showInfo.height, View.MeasureSpec.EXACTLY); } else { needMeasureForHeight = true; showInfo.contentHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( proxyHeight(maxHeight), View.MeasureSpec.AT_MOST); } } if (mForceMeasureIfNeeded && (needMeasureForWidth || needMeasureForHeight)) { mContentView.measure( showInfo.contentWidthMeasureSpec, showInfo.contentHeightMeasureSpec); if (needMeasureForWidth) { showInfo.width = proxyWidth(mContentView.getMeasuredWidth()); } if (needMeasureForHeight) { showInfo.height = proxyHeight(mContentView.getMeasuredHeight()); } } } private void setAnimationStyle(float anchorProportion, @Direction int direction) { boolean onTop = direction == DIRECTION_TOP; switch (mAnimStyle) { case ANIM_GROW_FROM_LEFT: mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Left : R.style.QMUI_Animation_PopDownMenu_Left); break; case ANIM_GROW_FROM_RIGHT: mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Right : R.style.QMUI_Animation_PopDownMenu_Right); break; case ANIM_GROW_FROM_CENTER: mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Center : R.style.QMUI_Animation_PopDownMenu_Center); break; case ANIM_AUTO: if (anchorProportion <= 0.25f) { mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Left : R.style.QMUI_Animation_PopDownMenu_Left); } else if (anchorProportion > 0.25f && anchorProportion < 0.75f) { mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Center : R.style.QMUI_Animation_PopDownMenu_Center); } else { mWindow.setAnimationStyle(onTop ? R.style.QMUI_Animation_PopUpMenu_Right : R.style.QMUI_Animation_PopDownMenu_Right); } break; case ANIM_SPEC: mWindow.setAnimationStyle(mSpecAnimStyle); break; } } static class ContentView extends QMUIFrameLayout { private ContentView(Context context) { super(context); } static ContentView wrap(View businessView, int width, int height) { ContentView contentView = new ContentView(businessView.getContext()); if (businessView.getParent() != null) { ((ViewGroup) businessView.getParent()).removeView(businessView); } contentView.addView(businessView, new LayoutParams(width, height)); return contentView; } } class DecorRootView extends FrameLayout implements IQMUISkinDispatchInterceptor { private ShowInfo mShowInfo; private View mContentView; private Paint mArrowPaint; private Path mArrowPath; private RectF mArrowSaveRect = new RectF(); private PorterDuffXfermode mArrowAlignMode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); private int mPendingWidth; private int mPendingHeight; private Runnable mUpdateWindowAction = new Runnable() { @Override public void run() { mShowInfo.width = mPendingWidth; mShowInfo.height = mPendingHeight; calculateXY(mShowInfo); adjustShowInfo(mShowInfo); mWindow.update(mShowInfo.getWindowX(), mShowInfo.getWindowY(), mShowInfo.windowWidth(), mShowInfo.windowHeight()); } }; private DecorRootView(Context context) { super(context); mArrowPaint = new Paint(); mArrowPaint.setAntiAlias(true); mArrowPath = new Path(); } public void setShowInfo(ShowInfo showInfo) { mShowInfo = showInfo; requestFocus(); } public void setContentView(View contentView) { if (mContentView != null) { removeView(mContentView); } if (contentView.getParent() != null) { ((ViewGroup) contentView.getParent()).removeView(contentView); } mContentView = contentView; addView(contentView); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { removeCallbacks(mUpdateWindowAction); if(mShowInfo == null){ setMeasuredDimension(0, 0); return; } if (mContentView != null) { mContentView.measure(mShowInfo.contentWidthMeasureSpec, mShowInfo.contentHeightMeasureSpec); int measuredWidth = mContentView.getMeasuredWidth(); int measuredHeight = mContentView.getMeasuredHeight(); if (mShowInfo.width != measuredWidth || mShowInfo.height != measuredHeight) { mPendingWidth = measuredWidth; mPendingHeight = measuredHeight; post(mUpdateWindowAction); } } setMeasuredDimension(mShowInfo.windowWidth(), mShowInfo.windowHeight()); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (mContentView != null && mShowInfo != null) { mContentView.layout(mShowInfo.decorationLeft, mShowInfo.decorationTop, mShowInfo.width + mShowInfo.decorationLeft, mShowInfo.height + mShowInfo.decorationTop); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); removeCallbacks(mUpdateWindowAction); } @Override public boolean intercept(int skinIndex, @NotNull Resources.Theme theme) { if (!mIsBorderColorSet && mBorderColorAttr != 0) { mBorderUsedColor = QMUIResHelper.getAttrColor(theme, mBorderColorAttr); } if (!mIsBgColorSet && mBgColorAttr != 0) { mBgUsedColor = QMUIResHelper.getAttrColor(theme, mBgColorAttr); } return false; } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if(mShowInfo == null){ return; } if (mShowArrow) { if (mShowInfo.direction == DIRECTION_TOP) { canvas.save(); mArrowSaveRect.set(0f, 0f, mShowInfo.width, mShowInfo.height); mArrowPaint.setStyle(Paint.Style.FILL); mArrowPaint.setColor(mBgUsedColor); mArrowPaint.setXfermode(null); int l = mShowInfo.anchorCenter - mShowInfo.x - mArrowWidth / 2; l = Math.min(Math.max(l, mShowInfo.decorationLeft), getWidth() - mShowInfo.decorationRight - mArrowWidth); int t = mShowInfo.decorationTop + mShowInfo.height - mBorderWidth; canvas.translate(l, t); mArrowPath.reset(); mArrowPath.setLastPoint(-mArrowWidth / 2f, -mArrowHeight); mArrowPath.lineTo(mArrowWidth / 2f, mArrowHeight); mArrowPath.lineTo(mArrowWidth * 3 /2f, -mArrowHeight); mArrowPath.close(); canvas.drawPath(mArrowPath, mArrowPaint); if (!mRemoveBorderWhenShadow || !shouldShowShadow()) { mArrowSaveRect.set(0f, -mBorderWidth, mArrowWidth, mArrowHeight + mBorderWidth); int saveLayer = canvas.saveLayer(mArrowSaveRect, mArrowPaint, Canvas.ALL_SAVE_FLAG); mArrowPaint.setStrokeWidth(mBorderWidth); mArrowPaint.setColor(mBorderUsedColor); mArrowPaint.setStyle(Paint.Style.STROKE); canvas.drawPath(mArrowPath, mArrowPaint); mArrowPaint.setXfermode(mArrowAlignMode); mArrowPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0f, -mBorderWidth, mArrowWidth, 0, mArrowPaint); canvas.restoreToCount(saveLayer); } canvas.restore(); } else if (mShowInfo.direction == DIRECTION_BOTTOM) { canvas.save(); mArrowPaint.setStyle(Paint.Style.FILL); mArrowPaint.setXfermode(null); mArrowPaint.setColor(mBgUsedColor); int l = mShowInfo.anchorCenter - mShowInfo.x - mArrowWidth / 2; l = Math.min(Math.max(l, mShowInfo.decorationLeft), getWidth() - mShowInfo.decorationRight - mArrowWidth); int t = mShowInfo.decorationTop + mBorderWidth; canvas.translate(l, t); mArrowPath.reset(); mArrowPath.setLastPoint(-mArrowWidth / 2f, mArrowHeight); mArrowPath.lineTo(mArrowWidth / 2f, -mArrowHeight); mArrowPath.lineTo(mArrowWidth * 3 / 2f, mArrowHeight); mArrowPath.close(); canvas.drawPath(mArrowPath, mArrowPaint); if (!mRemoveBorderWhenShadow || !shouldShowShadow()) { mArrowSaveRect.set(0, -mArrowHeight - mBorderWidth, mArrowWidth, mBorderWidth); int saveLayer = canvas.saveLayer(mArrowSaveRect, mArrowPaint, Canvas.ALL_SAVE_FLAG); mArrowPaint.setStrokeWidth(mBorderWidth); mArrowPaint.setStyle(Paint.Style.STROKE); mArrowPaint.setColor(mBorderUsedColor); canvas.drawPath(mArrowPath, mArrowPaint); mArrowPaint.setXfermode(mArrowAlignMode); mArrowPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0, 0, mArrowWidth, mBorderWidth, mArrowPaint); canvas.restoreToCount(saveLayer); } canvas.restore(); } } } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopup.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.widget.popup; import android.content.Context; import android.view.View; import androidx.annotation.NonNull; public class QMUIPopup extends QMUINormalPopup { public QMUIPopup(Context context, int width, int height) { this(context, width, height, true); } public QMUIPopup(Context context, int width, int height, boolean forceMeasureIfNeeded) { super(context, width, height, forceMeasureIfNeeded); } @Override public QMUIPopup show(@NonNull View anchor) { return super.show(anchor); } @Override public QMUIPopup show(@NonNull View anchor, int anchorAreaLeft, int anchorAreaTop, int anchorAreaRight, int anchorAreaBottom) { return super.show(anchor, anchorAreaLeft, anchorAreaTop, anchorAreaRight, anchorAreaBottom); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIPopups.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.widget.popup; import android.content.Context; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ListView; import com.qmuiteam.qmui.widget.QMUIWrapContentListView; public class QMUIPopups { public static QMUIPopup popup(Context context) { return new QMUIPopup(context, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } public static QMUIPopup popup(Context context, int width) { return new QMUIPopup(context, width, ViewGroup.LayoutParams.WRAP_CONTENT); } public static QMUIPopup popup(Context context, int width, int height) { return new QMUIPopup(context, width, height); } /** * show a list with popup * * @param context activity context * @param width the with for the popup content * @param maxHeight the max height of popup, it is scrollable if the content is higher then maxHeight * @param adapter the adapter for the list view * @param onItemClickListener the onItemClickListener for list item view * @return QMUIPopup */ public static QMUIPopup listPopup(Context context, int width, int maxHeight, BaseAdapter adapter, AdapterView.OnItemClickListener onItemClickListener) { ListView listView = new QMUIWrapContentListView(context, maxHeight); listView.setAdapter(adapter); listView.setVerticalScrollBarEnabled(false); listView.setOnItemClickListener(onItemClickListener); listView.setDivider(null); return popup(context, width).view(listView); } public static QMUIFullScreenPopup fullScreenPopup(Context context) { return new QMUIFullScreenPopup(context); } public static QMUIQuickAction quickAction(Context context, int actionWidth, int actionHeight) { return new QMUIQuickAction(context, ViewGroup.LayoutParams.WRAP_CONTENT, actionHeight) .actionWidth(actionWidth) .actionHeight(actionHeight); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/popup/QMUIQuickAction.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.widget.popup; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.QMUIConstraintLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView2; import java.util.ArrayList; import java.util.Objects; public class QMUIQuickAction extends QMUINormalPopup { private ArrayList mActions = new ArrayList<>(); private int mActionWidth = ViewGroup.LayoutParams.WRAP_CONTENT; private int mActionHeight; private boolean mShowMoreArrowIfNeeded = true; private int mMoreArrowWidth; private int mPaddingHor; public QMUIQuickAction(Context context, int width, int height){ this(context, width, height, true); } public QMUIQuickAction(Context context, int width, int height, boolean forceMeasureIfNeeded) { super(context, width, height, forceMeasureIfNeeded); mActionHeight = height; mMoreArrowWidth = QMUIResHelper.getAttrDimen(context, R.attr.qmui_quick_action_more_arrow_width); mPaddingHor = QMUIResHelper.getAttrDimen(context, R.attr.qmui_quick_action_padding_hor); } public QMUIQuickAction moreArrowWidth(int moreArrowWidth) { mMoreArrowWidth = moreArrowWidth; return this; } public QMUIQuickAction paddingHor(int paddingHor) { mPaddingHor = paddingHor; return this; } public QMUIQuickAction actionWidth(int actionWidth) { mActionWidth = actionWidth; return this; } public QMUIQuickAction actionHeight(int actionHeight) { mActionHeight = actionHeight; return this; } public QMUIQuickAction addAction(Action action) { mActions.add(action); return this; } public QMUIQuickAction showMoreArrowIfNeeded(boolean showMoreArrowIfNeeded) { mShowMoreArrowIfNeeded = showMoreArrowIfNeeded; return this; } @Override protected int proxyWidth(int width) { if (width > 0 && mActionWidth > 0) { if (width >= mActionWidth * mActions.size() + 2 * mPaddingHor) { return super.proxyWidth(width); } width = width - mPaddingHor - mMoreArrowWidth; return mActionWidth * (width / mActionWidth) + mPaddingHor + mMoreArrowWidth; } return super.proxyWidth(width); } @Override public QMUIQuickAction show(@NonNull View anchor) { return super.show(anchor); } @Override public QMUIQuickAction show(@NonNull View anchor, int anchorAreaLeft, int anchorAreaTop, int anchorAreaRight, int anchorAreaBottom) { view(createContentView()); return super.show(anchor, anchorAreaLeft, anchorAreaTop, anchorAreaRight, anchorAreaBottom); } private ConstraintLayout createContentView() { ConstraintLayout wrapper = new ConstraintLayout(mContext); final RecyclerView recyclerView = new RecyclerView(mContext); final LayoutManager layoutManager = new LayoutManager(mContext); recyclerView.setLayoutManager(layoutManager); recyclerView.setId(View.generateViewId()); recyclerView.setPadding(mPaddingHor, 0, mPaddingHor, 0); recyclerView.setClipToPadding(false); final Adapter adapter = new Adapter(); adapter.submitList(mActions); recyclerView.setAdapter(adapter); wrapper.addView(recyclerView); if (mShowMoreArrowIfNeeded) { AppCompatImageView leftMoreArrow = createMoreArrowView(true); AppCompatImageView rightMoreArrow = createMoreArrowView(false); leftMoreArrow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { recyclerView.smoothScrollToPosition(0); } }); rightMoreArrow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1); } }); ConstraintLayout.LayoutParams leftLp = new ConstraintLayout.LayoutParams(mMoreArrowWidth, 0); leftLp.leftToLeft = recyclerView.getId(); leftLp.topToTop = recyclerView.getId(); leftLp.bottomToBottom = recyclerView.getId(); wrapper.addView(leftMoreArrow, leftLp); ConstraintLayout.LayoutParams rightLp = new ConstraintLayout.LayoutParams(mMoreArrowWidth, 0); rightLp.rightToRight = recyclerView.getId(); rightLp.topToTop = recyclerView.getId(); rightLp.bottomToBottom = recyclerView.getId(); wrapper.addView(rightMoreArrow, rightLp); recyclerView.addItemDecoration(new ItemDecoration(leftMoreArrow, rightMoreArrow)); } return wrapper; } protected AppCompatImageView createMoreArrowView(boolean isLeft) { QMUIRadiusImageView2 arrowView = new QMUIRadiusImageView2(mContext); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (isLeft) { arrowView.setPadding(mPaddingHor, 0, 0, 0); builder.src(R.attr.qmui_skin_support_quick_action_more_left_arrow); } else { arrowView.setPadding(0, 0, mPaddingHor, 0); builder.src(R.attr.qmui_skin_support_quick_action_more_right_arrow); } builder.tintColor(R.attr.qmui_skin_support_quick_action_more_tint_color); int bgColor = getBgColor(); int bgColorAttr = getBgColorAttr(); if (bgColorAttr != 0) { builder.background(bgColorAttr); }else if (bgColor != Color.TRANSPARENT) { arrowView.setBackgroundColor(bgColor); } QMUISkinHelper.setSkinValue(arrowView, builder); arrowView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); arrowView.setVisibility(View.GONE); arrowView.setAlpha(0f); builder.release(); return arrowView; } private class ItemDecoration extends RecyclerView.ItemDecoration { private AppCompatImageView leftMoreArrowView; private AppCompatImageView rightMoreArrowView; private boolean isLeftMoreShown = false; private boolean isRightMoreShown = false; private boolean isFirstDraw = true; private int TOGGLE_DURATION = 60; public ItemDecoration(AppCompatImageView leftMoreArrowView, AppCompatImageView rightMoreArrowView) { this.leftMoreArrowView = leftMoreArrowView; this.rightMoreArrowView = rightMoreArrowView; } private Runnable leftHideEndAction = new Runnable() { @Override public void run() { leftMoreArrowView.setVisibility(View.GONE); } }; private Runnable rightHideEndAction = new Runnable() { @Override public void run() { rightMoreArrowView.setVisibility(View.GONE); } }; @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { if (parent.canScrollHorizontally(-1)) { if (!isLeftMoreShown) { isLeftMoreShown = true; leftMoreArrowView.setVisibility(View.VISIBLE); if (isFirstDraw) { leftMoreArrowView.setAlpha(1F); } else { leftMoreArrowView.animate() .alpha(1f) .setDuration(TOGGLE_DURATION) .start(); } } } else { if (isLeftMoreShown) { isLeftMoreShown = false; leftMoreArrowView.animate() .alpha(0f) .setDuration(TOGGLE_DURATION) .withEndAction(leftHideEndAction) .start(); } } if (parent.canScrollHorizontally(1)) { if (!isRightMoreShown) { isRightMoreShown = true; rightMoreArrowView.setVisibility(View.VISIBLE); if (isFirstDraw) { rightMoreArrowView.setAlpha(1F); } else { rightMoreArrowView.animate() .setDuration(TOGGLE_DURATION) .alpha(1f) .start(); } } } else { if (isRightMoreShown) { isRightMoreShown = false; rightMoreArrowView.animate() .alpha(0f) .setDuration(TOGGLE_DURATION) .withEndAction(rightHideEndAction) .start(); } } isFirstDraw = false; } } private class LayoutManager extends LinearLayoutManager { private static final float MILLISECONDS_PER_INCH = 0.01f; public LayoutManager(Context context) { super(context, LinearLayoutManager.HORIZONTAL, false); } @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(mActionWidth, mActionHeight); } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override protected int calculateTimeForScrolling(int dx) { return 100; } }; linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } } private static class VH extends RecyclerView.ViewHolder implements View.OnClickListener { private Callback callback; public VH(@NonNull ItemView itemView, @NonNull Callback callback) { super(itemView); itemView.setOnClickListener(this); this.callback = callback; } @Override public void onClick(View v) { callback.onClick(v, getAdapterPosition()); } interface Callback { void onClick(View v, int adapterPosition); } } private class DiffCallback extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull Action action, @NonNull Action t1) { return Objects.equals(action.text, t1.text) && action.icon == t1.icon && action.iconAttr == t1.iconAttr && action.onClickListener == t1.onClickListener; } @Override public boolean areContentsTheSame(@NonNull Action action, @NonNull Action t1) { return action.textColorAttr == t1.textColorAttr && action.iconTintColorAttr == t1.iconTintColorAttr; } } protected ItemView createItemView() { return new DefaultItemView(mContext); } private class Adapter extends ListAdapter implements VH.Callback { protected Adapter() { super(new DiffCallback()); } @NonNull @Override public VH onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new VH(createItemView(), this); } @Override public void onClick(View v, int adapterPosition) { Action action = getItem(adapterPosition); OnClickListener onClickListener = action.onClickListener; if (onClickListener != null) { onClickListener.onClick(QMUIQuickAction.this, action, adapterPosition); } } @Override public void onBindViewHolder(@NonNull VH vh, int i) { ItemView view = (ItemView) vh.itemView; view.render(getItem(i)); } } public static class Action { @Nullable Drawable icon; int iconRes; @Nullable OnClickListener onClickListener; @Nullable CharSequence text; int iconAttr = 0; int textColorAttr = R.attr.qmui_skin_support_quick_action_item_tint_color; int iconTintColorAttr = R.attr.qmui_skin_support_quick_action_item_tint_color; public Action iconAttr(int iconAttr) { this.iconAttr = iconAttr; return this; } public Action icon(Drawable icon) { this.icon = icon; return this; } public Action icon(int iconRes) { this.iconRes = iconRes; return this; } public Action onClick(OnClickListener onClickListener) { this.onClickListener = onClickListener; return this; } public Action text(CharSequence text) { this.text = text; return this; } public Action textColorAttr(int textColorAttr) { this.textColorAttr = textColorAttr; return this; } public Action iconTintColorAttr(int iconTintColorAttr) { this.iconTintColorAttr = iconTintColorAttr; return this; } } public interface OnClickListener { void onClick(QMUIQuickAction quickAction, Action action, int position); } public abstract static class ItemView extends QMUIConstraintLayout { public ItemView(Context context) { super(context); } public ItemView(Context context, AttributeSet attrs) { super(context, attrs); } public abstract void render(Action action); } public static class DefaultItemView extends ItemView { private AppCompatImageView mIconView; private TextView mTextView; public DefaultItemView(Context context) { this(context, null); } public DefaultItemView(Context context, AttributeSet attrs) { super(context, attrs); int paddingHor = QMUIResHelper.getAttrDimen( context, R.attr.qmui_quick_action_item_padding_hor); int paddingVer = QMUIResHelper.getAttrDimen( context, R.attr.qmui_quick_action_item_padding_ver); setPadding(paddingHor, paddingVer, paddingHor, paddingVer); mIconView = new AppCompatImageView(context); mIconView.setId(QMUIViewHelper.generateViewId()); mTextView = new TextView(context); mTextView.setId(QMUIViewHelper.generateViewId()); mTextView.setTextSize(10); mTextView.setTypeface(Typeface.DEFAULT_BOLD); setChangeAlphaWhenPress(true); setChangeAlphaWhenDisable(true); int wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT; LayoutParams iconLp = new LayoutParams(wrapContent, wrapContent); iconLp.leftToLeft = LayoutParams.PARENT_ID; iconLp.rightToRight = LayoutParams.PARENT_ID; iconLp.topToTop = LayoutParams.PARENT_ID; iconLp.bottomToTop = mTextView.getId(); iconLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; addView(mIconView, iconLp); LayoutParams textLp = new LayoutParams(wrapContent, wrapContent); textLp.leftToLeft = LayoutParams.PARENT_ID; textLp.rightToRight = LayoutParams.PARENT_ID; textLp.topToBottom = mIconView.getId(); textLp.bottomToBottom = LayoutParams.PARENT_ID; textLp.topMargin = QMUIResHelper.getAttrDimen( context, R.attr.qmui_quick_action_item_middle_space); textLp.verticalChainStyle = LayoutParams.CHAIN_PACKED; textLp.goneTopMargin = 0; addView(mTextView, textLp); } @Override public void render(Action action) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); if (action.icon != null || action.iconRes != 0) { if (action.icon != null) { mIconView.setImageDrawable(action.icon.mutate()); } else { mIconView.setImageResource(action.iconRes); } if (action.iconTintColorAttr != 0) { builder.tintColor(action.iconTintColorAttr); } mIconView.setVisibility(View.VISIBLE); QMUISkinHelper.setSkinValue(mIconView, builder); } else if (action.iconAttr != 0) { builder.src(action.iconAttr); mIconView.setVisibility(View.VISIBLE); QMUISkinHelper.setSkinValue(mIconView, builder); } else { mIconView.setVisibility(View.GONE); } mTextView.setText(action.text); builder.clear(); builder.textColor(action.textColorAttr); QMUISkinHelper.setSkinValue(mTextView, builder); builder.release(); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIAlwaysFollowOffsetCalculator.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.widget.pullLayout; public class QMUIAlwaysFollowOffsetCalculator implements QMUIPullLayout.ActionViewOffsetCalculator { @Override public int calculateOffset(QMUIPullLayout.PullAction pullAction, int targetOffset) { return targetOffset + pullAction.getActionInitOffset(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUICenterOffsetCalculator.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.widget.pullLayout; public class QMUICenterOffsetCalculator implements QMUIPullLayout.ActionViewOffsetCalculator { @Override public int calculateOffset(QMUIPullLayout.PullAction pullAction, int targetOffset) { if(targetOffset < pullAction.getTargetTriggerOffset()){ return targetOffset + pullAction.getActionInitOffset(); } return (targetOffset - pullAction.getActionPullSize()) / 2; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIFixToTargetOffsetCalculator.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.widget.pullLayout; public class QMUIFixToTargetOffsetCalculator implements QMUIPullLayout.ActionViewOffsetCalculator { @Override public int calculateOffset(QMUIPullLayout.PullAction pullAction, int targetOffset) { if (targetOffset < pullAction.getTargetTriggerOffset()) { return targetOffset + pullAction.getActionInitOffset(); } return pullAction.getTargetTriggerOffset() + pullAction.getActionInitOffset(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLayout.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.widget.pullLayout; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.OverScroller; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.NestedScrollingParent3; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.Beta; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import static com.qmuiteam.qmui.QMUIInterpolatorStaticHolder.QUNITIC_INTERPOLATOR; @Beta public class QMUIPullLayout extends FrameLayout implements NestedScrollingParent3 { public static final float DEFAULT_PULL_RATE = 0.45f; public static final float DEFAULT_FLING_FRACTION = 0.002f; public static final float DEFAULT_SCROLL_SPEED_PER_PIXEL = 1.5f; public static final int DEFAULT_MIN_SCROLL_DURATION = 300; public static final int PULL_EDGE_LEFT = 0x01; public static final int PULL_EDGE_TOP = 0x02; public static final int PULL_EDGE_RIGHT = 0x04; public static final int PULL_EDGE_BOTTOM = 0x08; public static final int PUL_EDGE_ALL = PULL_EDGE_LEFT | PULL_EDGE_TOP | PULL_EDGE_RIGHT | PULL_EDGE_BOTTOM; private static final int STATE_IDLE = 0; private static final int STATE_PULLING = 1; private static final int STATE_SETTLING_TO_TRIGGER_OFFSET = 2; private static final int STATE_TRIGGERING= 3; private static final int STATE_SETTLING_TO_INIT_OFFSET = 4; private static final int STATE_SETTLING_DELIVER = 5; private static final int STATE_SETTLING_FLING = 6; @IntDef({PULL_EDGE_LEFT, PULL_EDGE_TOP, PULL_EDGE_RIGHT, PULL_EDGE_BOTTOM}) @Retention(RetentionPolicy.SOURCE) public @interface PullEdge { } private int mEnabledEdges; private View mTargetView; private QMUIViewOffsetHelper mTargetOffsetHelper; private PullAction mLeftPullAction = null; private PullAction mTopPullAction = null; private PullAction mRightPullAction = null; private PullAction mBottomPullAction = null; private ActionListener mActionListener; // Array to be used for calls from v2 version of onNestedScroll to v3 version of onNestedScroll. // This only exist to prevent GC and object instantiation costs that are present before API 21. private final int[] mNestedScrollingV2ConsumedCompat = new int[2]; private StopTargetViewFlingImpl mStopTargetViewFlingImpl = DefaultStopTargetViewFlingImpl.getInstance(); private Runnable mStopTargetFlingRunnable = null; private OverScroller mScroller; private float mNestedPreFlingVelocityScaleDown = 10; private int mMinScrollDuration = DEFAULT_MIN_SCROLL_DURATION; private int mState = STATE_IDLE; private final NestedScrollingParentHelper mNestedScrollingParentHelper; public QMUIPullLayout(@NonNull Context context) { this(context, null); } public QMUIPullLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.QMUIPullLayoutStyle); } public QMUIPullLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIPullLayout, defStyleAttr, 0); mEnabledEdges = array.getInt(R.styleable.QMUIPullLayout_qmui_pull_enable_edge, PUL_EDGE_ALL); array.recycle(); mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mScroller = new OverScroller(context, QUNITIC_INTERPOLATOR); } @Override protected void onFinishInflate() { super.onFinishInflate(); boolean isTargetSet = false; int edgesSet = 0; for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.isTarget) { if (isTargetSet) { throw new RuntimeException( "More than one view in xml are marked by qmui_is_target = true."); } isTargetSet = true; setTargetView(view); } else { if ((edgesSet & lp.edge) != 0) { String text = ""; if (lp.edge == PULL_EDGE_LEFT) { text = "left"; } else if (lp.edge == PULL_EDGE_TOP) { text = "top"; } else if (lp.edge == PULL_EDGE_RIGHT) { text = "right"; } else if (lp.edge == PULL_EDGE_BOTTOM) { text = "bottom"; } throw new RuntimeException("More than one view in xml marked by qmui_layout_edge = " + text); } edgesSet |= lp.edge; setActionView(view, lp); } } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { if (mScroller.isFinished()) { if(mState == STATE_SETTLING_TO_INIT_OFFSET){ mState = STATE_IDLE; return; } if(mState == STATE_TRIGGERING){ return; } if(mState == STATE_SETTLING_FLING){ checkScrollToTargetOffsetOrInitOffset(false); return; } if(mState == STATE_SETTLING_TO_TRIGGER_OFFSET){ mState = STATE_TRIGGERING; if (mLeftPullAction != null && isEdgeEnabled(PULL_EDGE_LEFT)) { if (mScroller.getFinalX() == mLeftPullAction.getTargetTriggerOffset()) { onActionTriggered(mLeftPullAction); } } if (mRightPullAction != null && isEdgeEnabled(PULL_EDGE_RIGHT)) { if (mScroller.getFinalX() == -mRightPullAction.getTargetTriggerOffset()) { onActionTriggered(mRightPullAction); } } if (mTopPullAction != null && isEdgeEnabled(PULL_EDGE_TOP)) { if (mScroller.getFinalY() == mTopPullAction.getTargetTriggerOffset()) { onActionTriggered(mTopPullAction); } } if (mBottomPullAction != null && isEdgeEnabled(PULL_EDGE_BOTTOM)) { if (mScroller.getFinalY() == -mBottomPullAction.getTargetTriggerOffset()) { onActionTriggered(mBottomPullAction); } } setHorOffsetToTargetOffsetHelper(mScroller.getCurrX()); setVerOffsetToTargetOffsetHelper(mScroller.getCurrY()); } }else{ setHorOffsetToTargetOffsetHelper(mScroller.getCurrX()); setVerOffsetToTargetOffsetHelper(mScroller.getCurrY()); postInvalidateOnAnimation(); } } } public void setStopTargetViewFlingImpl(@NonNull StopTargetViewFlingImpl stopTargetViewFlingImpl) { mStopTargetViewFlingImpl = stopTargetViewFlingImpl; } public void setMinScrollDuration(int minScrollDuration) { mMinScrollDuration = minScrollDuration; } public void setTargetView(@NonNull View view) { if (view.getParent() != this) { throw new RuntimeException("Target already exists other parent view."); } if (view.getParent() == null) { LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); addView(view, lp); } innerSetTargetView(view); } private void innerSetTargetView(@NonNull View view) { mTargetView = view; mTargetOffsetHelper = new QMUIViewOffsetHelper(view); } public void setActionView(View view, LayoutParams lp) { PullActionBuilder builder = new PullActionBuilder(view, lp.edge) .canOverPull(lp.canOverPull) .pullRate(lp.pullRate) .needReceiveFlingFromTargetView(lp.needReceiveFlingFromTarget) .receivedFlingFraction(lp.receivedFlingFraction) .scrollSpeedPerPixel(lp.scrollSpeedPerPixel) .targetTriggerOffset(lp.targetTriggerOffset) .triggerUntilScrollToTriggerOffset(lp.triggerUntilScrollToTriggerOffset) .scrollToTriggerOffsetAfterTouchUp(lp.scrollToTriggerOffsetAfterTouchUp) .actionInitOffset(lp.actionInitOffset); view.setLayoutParams(lp); setActionView(builder); } public void setActionView(@NonNull PullActionBuilder builder) { if (builder.mActionView.getParent() != this) { throw new RuntimeException("Action view already exists other parent view."); } if (builder.mActionView.getParent() == null) { ViewGroup.LayoutParams lp = builder.mActionView.getLayoutParams(); if (lp == null) { lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } addView(builder.mActionView, lp); } if (builder.mPullEdge == PULL_EDGE_LEFT) { mLeftPullAction = builder.build(); } else if (builder.mPullEdge == PULL_EDGE_TOP) { mTopPullAction = builder.build(); } else if (builder.mPullEdge == PULL_EDGE_RIGHT) { mRightPullAction = builder.build(); } else if (builder.mPullEdge == PULL_EDGE_BOTTOM) { mBottomPullAction = builder.build(); } } public void setActionListener(ActionListener actionListener) { mActionListener = actionListener; } public void setEnabledEdges(int enabledEdges) { mEnabledEdges = enabledEdges; } public boolean isEdgeEnabled(@PullEdge int edge) { return (mEnabledEdges & edge) == edge && getPullAction(edge) != null; } @Nullable private PullAction getPullAction(@PullEdge int edge) { if (edge == PULL_EDGE_LEFT) { return mLeftPullAction; } else if (edge == PULL_EDGE_TOP) { return mTopPullAction; } else if (edge == PULL_EDGE_RIGHT) { return mRightPullAction; } else if (edge == PULL_EDGE_BOTTOM) { return mBottomPullAction; } return null; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int w = r - l; int h = b - t; if (mTargetView != null) { mTargetView.layout(0, 0, w, h); mTargetOffsetHelper.onViewLayout(); } if (mLeftPullAction != null) { View view = mLeftPullAction.mActionView; int vw = view.getMeasuredWidth(), vh = view.getMeasuredHeight(), vc = (h - vh) / 2; view.layout(-vw, vc, 0, vc + vh); mLeftPullAction.mViewOffsetHelper.onViewLayout(); } if (mTopPullAction != null) { View view = mTopPullAction.mActionView; int vw = view.getMeasuredWidth(), vh = view.getMeasuredHeight(), vc = (w - vw) / 2; view.layout(vc, -vh, vc + vw, 0); mTopPullAction.mViewOffsetHelper.onViewLayout(); } if (mRightPullAction != null) { View view = mRightPullAction.mActionView; int vw = view.getMeasuredWidth(), vh = view.getMeasuredHeight(), vc = (h - vh) / 2; view.layout(w, vc, w + vw, vc + vh); mRightPullAction.mViewOffsetHelper.onViewLayout(); } if (mBottomPullAction != null) { View view = mBottomPullAction.mActionView; int vw = view.getMeasuredWidth(), vh = view.getMeasuredHeight(), vc = (w - vw) / 2; view.layout(vc, h, vc + vw, h + vh); mBottomPullAction.mViewOffsetHelper.onViewLayout(); } } public void setNestedPreFlingVelocityScaleDown(float nestedPreFlingVelocityScaleDown) { mNestedPreFlingVelocityScaleDown = nestedPreFlingVelocityScaleDown; } @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return mTargetView == target && (axes == ViewCompat.SCROLL_AXIS_HORIZONTAL && (isEdgeEnabled(PULL_EDGE_LEFT) || isEdgeEnabled(PULL_EDGE_RIGHT))) || (axes == ViewCompat.SCROLL_AXIS_VERTICAL && (isEdgeEnabled(PULL_EDGE_TOP) || isEdgeEnabled(PULL_EDGE_BOTTOM))); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { if(type == ViewCompat.TYPE_TOUCH){ removeStopTargetFlingRunnable(); mScroller.abortAnimation(); mState = STATE_PULLING; } mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); } @Override public void onNestedScrollAccepted(View child, View target, int axes) { onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH); } @Override public void onNestedPreScroll(@NonNull final View target, int dx, int dy, @NonNull int[] consumed, int type) { int originDx = dx, originDy = dy; dy = checkEdgeTopScrollDown(dy, consumed, type); dy = checkEdgeBottomScrollDown(dy, consumed, type); dy = checkEdgeTopScrollUp(dy, consumed, type); dy = checkEdgeBottomScrollUp(dy, consumed, type); dx = checkEdgeLeftScrollRight(dx, consumed, type); dx = checkEdgeRightScrollRight(dx, consumed, type); dx = checkEdgeLeftScrollLeft(dx, consumed, type); dx = checkEdgeRightScrollLeft(dx, consumed, type); if(originDx == dx && originDy == dy && mState == STATE_SETTLING_DELIVER){ checkStopTargetFling(target, dx, dy, type); } } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) { int originDxUnconsumed = dxUnconsumed, originDyUnconsumed = dyUnconsumed; dyUnconsumed = checkEdgeTopScrollDown(dyUnconsumed, consumed, type); dyUnconsumed = checkEdgeBottomScrollDown(dyUnconsumed, consumed, type); dyUnconsumed = checkEdgeTopScrollUp(dyUnconsumed, consumed, type); dyUnconsumed = checkEdgeBottomScrollUp(dyUnconsumed, consumed, type); dxUnconsumed = checkEdgeLeftScrollRight(dxUnconsumed, consumed, type); dxUnconsumed = checkEdgeRightScrollRight(dxUnconsumed, consumed, type); dxUnconsumed = checkEdgeLeftScrollLeft(dxUnconsumed, consumed, type); dxUnconsumed = checkEdgeRightScrollLeft(dxUnconsumed, consumed, type); if(dyUnconsumed == originDyUnconsumed && dxUnconsumed == originDxUnconsumed && mState == STATE_SETTLING_DELIVER){ checkStopTargetFling(target, dxUnconsumed, dyUnconsumed, type); } } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, mNestedScrollingV2ConsumedCompat); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); // if the targetView is RecyclerView and we set OnFlingListener for RecyclerView. // then the targetView can not deliver fling consume to NestedScrollParent // so we intercept the fling if the target view can not consume the fling. if(mLeftPullAction != null && isEdgeEnabled(PULL_EDGE_LEFT)){ if(velocityX < 0 && !mTargetView.canScrollHorizontally(-1)){ mState = STATE_SETTLING_FLING; velocityX /= mNestedPreFlingVelocityScaleDown; int maxX = mLeftPullAction.isCanOverPull() ? Integer.MAX_VALUE : mLeftPullAction.getTargetTriggerOffset(); mScroller.fling(hOffset, vOffset, (int) -velocityX, 0, 0, maxX, vOffset, vOffset); postInvalidateOnAnimation(); return true; }else if(velocityX > 0 && hOffset > 0){ mState = STATE_SETTLING_TO_INIT_OFFSET; mScroller.startScroll(hOffset, vOffset, -hOffset, 0, scrollDuration(mLeftPullAction,hOffset)); postInvalidateOnAnimation(); return true; } } if(mRightPullAction != null && isEdgeEnabled(PULL_EDGE_RIGHT)){ if(velocityX > 0 && !mTargetView.canScrollHorizontally(1)){ mState = STATE_SETTLING_FLING; velocityX /= mNestedPreFlingVelocityScaleDown; int minX = mRightPullAction.isCanOverPull() ? Integer.MIN_VALUE : -mRightPullAction.getTargetTriggerOffset(); mScroller.fling(hOffset, vOffset, (int) -velocityX, 0, minX, 0, vOffset, vOffset); postInvalidateOnAnimation(); return true; }else if(velocityX < 0 && hOffset < 0){ mState = STATE_SETTLING_TO_INIT_OFFSET; mScroller.startScroll(hOffset, vOffset, -hOffset, 0, scrollDuration(mRightPullAction, hOffset)); postInvalidateOnAnimation(); return true; } } if(mTopPullAction != null && isEdgeEnabled(PULL_EDGE_TOP)){ if(velocityY < 0 && !mTargetView.canScrollVertically(-1)){ mState = STATE_SETTLING_FLING; velocityY /= mNestedPreFlingVelocityScaleDown; int maxY = mTopPullAction.isCanOverPull() ? Integer.MAX_VALUE : mTopPullAction.getTargetTriggerOffset(); mScroller.fling(hOffset, vOffset, 0, (int) -velocityY, hOffset, hOffset, 0, maxY); postInvalidateOnAnimation(); return true; }else if(velocityY > 0 && vOffset > 0){ mState = STATE_SETTLING_TO_INIT_OFFSET; mScroller.startScroll(hOffset, vOffset, 0, -vOffset, scrollDuration(mTopPullAction, vOffset)); postInvalidateOnAnimation(); return true; } } if(mBottomPullAction != null && isEdgeEnabled(PULL_EDGE_BOTTOM)){ if(velocityY > 0 && !mTargetView.canScrollVertically(1)){ mState = STATE_SETTLING_FLING; velocityY /= mNestedPreFlingVelocityScaleDown; int minY = mBottomPullAction.isCanOverPull() ? Integer.MIN_VALUE : -mBottomPullAction.getTargetTriggerOffset(); mScroller.fling(hOffset, vOffset, 0, (int) -velocityY, hOffset, hOffset, minY, 0); postInvalidateOnAnimation(); return true; }else if(velocityY < 0 && vOffset < 0){ mState = STATE_SETTLING_TO_INIT_OFFSET; mScroller.startScroll(hOffset, vOffset, 0, -vOffset, scrollDuration(mBottomPullAction, vOffset)); postInvalidateOnAnimation(); return true; } } mState = STATE_SETTLING_DELIVER; return super.onNestedPreFling(target, velocityX, velocityY); } @Override public void onStopNestedScroll(@NonNull View target, int type) { if(mState == STATE_PULLING){ checkScrollToTargetOffsetOrInitOffset(false); }else if(mState == STATE_SETTLING_DELIVER && type != ViewCompat.TYPE_TOUCH){ removeStopTargetFlingRunnable(); checkScrollToTargetOffsetOrInitOffset(false); } } private int scrollDuration(PullAction pullAction, int delta){ return Math.max(mMinScrollDuration, Math.abs((int) (pullAction.mScrollSpeedPerPixel * delta))); } private void onActionTriggered(PullAction pullAction) { if(pullAction.mIsActionRunning){ return; } pullAction.mIsActionRunning = true; if(mActionListener != null){ mActionListener.onActionTriggered(pullAction); } if(pullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)pullAction.mActionView).onActionTriggered(); } } public void finishActionRun(@NonNull PullAction pullAction){ finishActionRun(pullAction, true); } public void finishActionRun(@NonNull PullAction pullAction, boolean animate){ if(pullAction != getPullAction(pullAction.mPullEdge)){ return; } pullAction.mIsActionRunning = false; if(pullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)pullAction.mActionView).onActionFinished(); } if(mState == STATE_PULLING){ return; } if(!animate){ mState = STATE_IDLE; setVerOffsetToTargetOffsetHelper(0); setHorOffsetToTargetOffsetHelper(0); return; } mState = STATE_SETTLING_TO_INIT_OFFSET; @PullEdge int pullEdge = pullAction.getPullEdge(); int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); if(pullEdge == PULL_EDGE_TOP && mTopPullAction != null && vOffset > 0){ mScroller.startScroll(hOffset, vOffset, 0, -vOffset, scrollDuration(mTopPullAction, vOffset)); postInvalidateOnAnimation(); }else if(pullEdge == PULL_EDGE_BOTTOM && mBottomPullAction != null && vOffset < 0){ mScroller.startScroll(hOffset, vOffset, 0, -vOffset, scrollDuration(mBottomPullAction, vOffset)); postInvalidateOnAnimation(); }else if(pullEdge == PULL_EDGE_LEFT && mLeftPullAction != null && hOffset > 0){ mScroller.startScroll(hOffset, vOffset, -hOffset, 0, scrollDuration(mLeftPullAction, hOffset)); postInvalidateOnAnimation(); }else if(pullEdge == PULL_EDGE_RIGHT && mRightPullAction != null && hOffset < 0){ mScroller.startScroll(hOffset, vOffset, -hOffset, 0, scrollDuration(mRightPullAction, hOffset)); postInvalidateOnAnimation(); } } private void checkScrollToTargetOffsetOrInitOffset(boolean forceInit) { if (mTargetView == null) { return; } mScroller.abortAnimation(); int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); int hTarget = 0, vTarget = 0; if (mLeftPullAction != null && isEdgeEnabled(PULL_EDGE_LEFT) && hOffset > 0) { mState = STATE_SETTLING_TO_INIT_OFFSET; if(!forceInit){ int targetOffset = mLeftPullAction.getTargetTriggerOffset(); if(hOffset == targetOffset){ onActionTriggered(mLeftPullAction); return; } if(hOffset > targetOffset){ if(!mLeftPullAction.mScrollToTriggerOffsetAfterTouchUp){ mState = STATE_TRIGGERING; onActionTriggered(mLeftPullAction); return; } if(!mLeftPullAction.mTriggerUntilScrollToTriggerOffset){ mState = STATE_TRIGGERING; onActionTriggered(mLeftPullAction); }else{ mState = STATE_SETTLING_TO_TRIGGER_OFFSET; } hTarget = targetOffset; } } int dx = hTarget - hOffset; mScroller.startScroll(hOffset, vOffset, dx, 0, scrollDuration(mLeftPullAction, dx)); postInvalidateOnAnimation(); return; } if(mRightPullAction != null && isEdgeEnabled(PULL_EDGE_RIGHT) && hOffset < 0){ mState = STATE_SETTLING_TO_INIT_OFFSET; if(!forceInit){ int targetOffset = mRightPullAction.getTargetTriggerOffset(); if (hOffset == -targetOffset) { mState = STATE_TRIGGERING; onActionTriggered(mRightPullAction); return; } if(hOffset < -targetOffset){ if(!mRightPullAction.mScrollToTriggerOffsetAfterTouchUp){ mState = STATE_TRIGGERING; onActionTriggered(mRightPullAction); return; } if(!mRightPullAction.mTriggerUntilScrollToTriggerOffset){ mState = STATE_TRIGGERING; onActionTriggered(mRightPullAction); }else{ mState = STATE_SETTLING_TO_TRIGGER_OFFSET; } hTarget = -targetOffset; } } int dx = hTarget - hOffset; mScroller.startScroll(hOffset, vOffset, dx, 0,scrollDuration(mRightPullAction, dx)); postInvalidateOnAnimation(); return; } if (mTopPullAction != null && isEdgeEnabled(PULL_EDGE_TOP) && vOffset > 0) { mState = STATE_SETTLING_TO_INIT_OFFSET; if(!forceInit){ int targetOffset = mTopPullAction.getTargetTriggerOffset(); if(vOffset == targetOffset){ mState = STATE_TRIGGERING; onActionTriggered(mTopPullAction); return; } if(vOffset > targetOffset){ if(!mTopPullAction.mScrollToTriggerOffsetAfterTouchUp){ mState = STATE_TRIGGERING; onActionTriggered(mTopPullAction); return; } if(!mTopPullAction.mTriggerUntilScrollToTriggerOffset){ mState = STATE_TRIGGERING; onActionTriggered(mTopPullAction); }else{ mState = STATE_SETTLING_TO_TRIGGER_OFFSET; } vTarget = targetOffset; } } int dy = vTarget - vOffset; mScroller.startScroll(hOffset, vOffset, hOffset, dy, scrollDuration(mTopPullAction, dy)); postInvalidateOnAnimation(); return; } if (mBottomPullAction != null && isEdgeEnabled(PULL_EDGE_BOTTOM) && vOffset < 0) { mState = STATE_SETTLING_TO_INIT_OFFSET; if(!forceInit){ int targetOffset = mBottomPullAction.getTargetTriggerOffset(); if(vOffset == -targetOffset){ onActionTriggered(mBottomPullAction); return; } if(vOffset < -targetOffset){ if(!mBottomPullAction.mScrollToTriggerOffsetAfterTouchUp){ mState = STATE_TRIGGERING; onActionTriggered(mBottomPullAction); return; } if(!mBottomPullAction.mTriggerUntilScrollToTriggerOffset){ mState = STATE_TRIGGERING; onActionTriggered(mBottomPullAction); }else{ mState = STATE_SETTLING_TO_TRIGGER_OFFSET; } vTarget = -targetOffset; } } int dy = vTarget - vOffset; mScroller.startScroll(hOffset, vOffset, hOffset, dy, scrollDuration(mBottomPullAction, dy)); postInvalidateOnAnimation(); return; } mState = STATE_IDLE; } private void removeStopTargetFlingRunnable() { if (mStopTargetFlingRunnable != null) { removeCallbacks(mStopTargetFlingRunnable); mStopTargetFlingRunnable = null; } } private void checkStopTargetFling(final View targetView, int dx, int dy, int type) { if (mStopTargetFlingRunnable != null || type == ViewCompat.TYPE_TOUCH) { return; } if ((dy < 0 && !mTargetView.canScrollVertically(-1)) || (dy > 0 && !mTargetView.canScrollVertically(1)) || (dx < 0 && !mTargetView.canScrollHorizontally(-1)) || (dx > 0 && !mTargetView.canScrollHorizontally(1))) { mStopTargetFlingRunnable = new Runnable() { @Override public void run() { mStopTargetViewFlingImpl.stopFling(targetView); mStopTargetFlingRunnable = null; checkScrollToTargetOffsetOrInitOffset(false); } }; post(mStopTargetFlingRunnable); } } private void setHorOffsetToTargetOffsetHelper(int hOffset) { mTargetOffsetHelper.setLeftAndRightOffset(hOffset); onTargetViewLeftAndRightOffsetChanged(hOffset); if (mLeftPullAction != null) { mLeftPullAction.onTargetMoved(hOffset); if(mLeftPullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)mLeftPullAction.mActionView).onPull(mLeftPullAction, hOffset); } } if (mRightPullAction != null) { mRightPullAction.onTargetMoved(-hOffset); if(mRightPullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)mRightPullAction.mActionView).onPull(mRightPullAction, -hOffset); } } } private void setVerOffsetToTargetOffsetHelper(int vOffset) { mTargetOffsetHelper.setTopAndBottomOffset(vOffset); onTargetViewTopAndBottomOffsetChanged(vOffset); if (mTopPullAction != null) { mTopPullAction.onTargetMoved(vOffset); if(mTopPullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)mTopPullAction.mActionView).onPull(mTopPullAction, vOffset); } } if (mBottomPullAction != null) { mBottomPullAction.onTargetMoved(-vOffset); if(mBottomPullAction.mActionView instanceof ActionPullWatcherView){ ((ActionPullWatcherView)mBottomPullAction.mActionView).onPull(mBottomPullAction, -vOffset); } } } protected void onTargetViewTopAndBottomOffsetChanged(int vOffset){ } protected void onTargetViewLeftAndRightOffsetChanged(int hOffset){ } private int checkEdgeTopScrollDown(int dy, int[] consumed, int type) { int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); if (dy > 0 && isEdgeEnabled(PULL_EDGE_TOP) && vOffset > 0) { float pullRate = type == ViewCompat.TYPE_TOUCH ? mTopPullAction.getPullRate() : 1f; int ry = (int) (dy * pullRate); if(ry == 0){ return dy; } if (vOffset >= ry) { consumed[1] += dy; vOffset -= ry; dy = 0; } else { int yConsumed = (int) (vOffset / pullRate); consumed[1] += yConsumed; dy -= yConsumed; vOffset = 0; } setVerOffsetToTargetOffsetHelper(vOffset); } return dy; } private int checkEdgeTopScrollUp(int dy, int[] consumed, int type) { if (dy < 0 && isEdgeEnabled(PULL_EDGE_TOP) && !mTargetView.canScrollVertically(-1) && (type == ViewCompat.TYPE_TOUCH || mTopPullAction.mNeedReceiveFlingFromTargetView)) { int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); float pullRate = type == ViewCompat.TYPE_TOUCH ? mTopPullAction.getPullRate(): mTopPullAction.getFlingRate(vOffset); int ry = (int) (dy * pullRate); if(ry == 0){ return dy; } if (mTopPullAction.mCanOverPull || -ry <= mTopPullAction.getTargetTriggerOffset() - vOffset) { vOffset -= ry; consumed[1] += dy; dy = 0; } else { int yConsumed = (int) ((vOffset - mTopPullAction.getTargetTriggerOffset()) / pullRate); consumed[1] += yConsumed; dy -= yConsumed; vOffset = mBottomPullAction.getTargetTriggerOffset(); } setVerOffsetToTargetOffsetHelper(vOffset); } return dy; } private int checkEdgeBottomScrollDown(int dy, int[] consumed, int type) { if (dy > 0 && isEdgeEnabled(PULL_EDGE_BOTTOM) && !mTargetView.canScrollVertically(1) && (type == ViewCompat.TYPE_TOUCH || mBottomPullAction.mNeedReceiveFlingFromTargetView)) { int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); float pullRate =type == ViewCompat.TYPE_TOUCH ? mBottomPullAction.getPullRate(): mBottomPullAction.getFlingRate(-vOffset); int ry = (int) (dy * pullRate); if(ry == 0){ return dy; } if (mBottomPullAction.mCanOverPull || vOffset - ry >= -mBottomPullAction.getTargetTriggerOffset()) { vOffset -= ry; consumed[1] += dy; dy = 0; } else { int yConsumed = (int) ((-mBottomPullAction.getTargetTriggerOffset() - vOffset) / pullRate); consumed[1] += yConsumed; dy -= yConsumed; vOffset = -mBottomPullAction.getTargetTriggerOffset(); } setVerOffsetToTargetOffsetHelper(vOffset); } return dy; } private int checkEdgeBottomScrollUp(int dy, int[] consumed, int type) { int vOffset = mTargetOffsetHelper.getTopAndBottomOffset(); if (dy < 0 && isEdgeEnabled(PULL_EDGE_BOTTOM) && vOffset < 0) { float pullRate = type == ViewCompat.TYPE_TOUCH ? mBottomPullAction.getPullRate() : 1f; int ry = (int) (dy * pullRate); if(ry == 0){ return dy; } if (vOffset <= ry) { consumed[1] += dy; vOffset -= ry; dy = 0; } else { int yConsumed = (int) (vOffset / pullRate); consumed[1] += yConsumed; dy -= yConsumed; vOffset = 0; } setVerOffsetToTargetOffsetHelper(vOffset); } return dy; } private int checkEdgeLeftScrollRight(int dx, int[] consumed, int type) { int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); if (dx > 0 && isEdgeEnabled(PULL_EDGE_LEFT) && hOffset > 0) { float pullRate = type == ViewCompat.TYPE_TOUCH ? mLeftPullAction.getPullRate(): 1f; int rx = (int) (dx * pullRate); if(rx == 0){ return dx; } if (hOffset >= rx) { consumed[0] += dx; hOffset -= rx; dx = 0; } else { int xConsumed = (int) (hOffset / pullRate); consumed[0] += xConsumed; dx -= xConsumed; hOffset = 0; } setHorOffsetToTargetOffsetHelper(hOffset); } return dx; } private int checkEdgeLeftScrollLeft(int dx, int[] consumed, int type) { int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); if (dx < 0 && isEdgeEnabled(PULL_EDGE_LEFT) && !mTargetView.canScrollHorizontally(-1) && (type == ViewCompat.TYPE_TOUCH || mLeftPullAction.mNeedReceiveFlingFromTargetView)) { float pullRate =type == ViewCompat.TYPE_TOUCH ? mLeftPullAction.getPullRate(): mLeftPullAction.getFlingRate(hOffset); int rx = (int) (dx * pullRate); if(rx == 0){ return dx; } if (mLeftPullAction.mCanOverPull || -rx <= mLeftPullAction.getTargetTriggerOffset() - hOffset) { hOffset -= rx; consumed[0] += dx; dx = 0; } else { int xConsumed = (int) ((hOffset - mLeftPullAction.getTargetTriggerOffset()) / pullRate); consumed[0] += xConsumed; dx -= xConsumed; hOffset = mLeftPullAction.getTargetTriggerOffset(); } setHorOffsetToTargetOffsetHelper(hOffset); } return dx; } private int checkEdgeRightScrollRight(int dx, int[] consumed, int type) { if (dx > 0 && isEdgeEnabled(PULL_EDGE_RIGHT) && !mTargetView.canScrollHorizontally(1) && (type == ViewCompat.TYPE_TOUCH || mRightPullAction.mNeedReceiveFlingFromTargetView)) { int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); float pullRate = type == ViewCompat.TYPE_TOUCH ? mRightPullAction.getPullRate(): mRightPullAction.getFlingRate(-hOffset); int rx = (int) (dx * pullRate); if(rx == 0){ return dx; } if (mRightPullAction.mCanOverPull || hOffset - rx >= -mRightPullAction.getTargetTriggerOffset()) { hOffset -= rx; consumed[0] += dx; dx = 0; } else { int xConsumed = (int) ((-mRightPullAction.getTargetTriggerOffset() - hOffset) / pullRate); consumed[0] += xConsumed; dx -= xConsumed; hOffset = -mRightPullAction.getTargetTriggerOffset(); } setHorOffsetToTargetOffsetHelper(hOffset); } return dx; } private int checkEdgeRightScrollLeft(int dx, int[] consumed, int type) { int hOffset = mTargetOffsetHelper.getLeftAndRightOffset(); if (dx < 0 && isEdgeEnabled(PULL_EDGE_RIGHT) && hOffset < 0) { float pullRate = type == ViewCompat.TYPE_TOUCH ? mRightPullAction.getPullRate(): 1f; int rx = (int) (dx * pullRate); if(rx == 0){ return dx; } if (hOffset <= dx) { consumed[0] += dx; hOffset -= rx; dx = 0; } else { int xConsumed = (int) (hOffset / pullRate); consumed[0] += xConsumed; dx -= xConsumed; hOffset = 0; } setHorOffsetToTargetOffsetHelper(hOffset); } return dx; } @Override protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { return new LayoutParams(lp); } @Override public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected FrameLayout.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } public static class LayoutParams extends FrameLayout.LayoutParams { public boolean isTarget = false; public int edge = PULL_EDGE_TOP; public int targetTriggerOffset = ViewGroup.LayoutParams.WRAP_CONTENT; public boolean canOverPull = false; public float pullRate = DEFAULT_PULL_RATE; public boolean needReceiveFlingFromTarget = true; public float receivedFlingFraction = DEFAULT_FLING_FRACTION; public int actionInitOffset = 0; public float scrollSpeedPerPixel = DEFAULT_SCROLL_SPEED_PER_PIXEL; public boolean triggerUntilScrollToTriggerOffset = false; public boolean scrollToTriggerOffsetAfterTouchUp = true; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.QMUIPullLayout_Layout); isTarget = a.getBoolean(R.styleable.QMUIPullLayout_Layout_qmui_is_target, false); if (!isTarget) { edge = a.getInteger(R.styleable.QMUIPullLayout_Layout_qmui_pull_edge, PULL_EDGE_TOP); try { targetTriggerOffset = a.getDimensionPixelSize( R.styleable.QMUIPullLayout_Layout_qmui_target_view_trigger_offset, ViewGroup.LayoutParams.WRAP_CONTENT); } catch (Exception ignore) { int intValue = a.getInt(R.styleable.QMUIPullLayout_Layout_qmui_target_view_trigger_offset, ViewGroup.LayoutParams.WRAP_CONTENT); if (intValue == ViewGroup.LayoutParams.WRAP_CONTENT) { targetTriggerOffset = ViewGroup.LayoutParams.WRAP_CONTENT; } } canOverPull = a.getBoolean( R.styleable.QMUIPullLayout_Layout_qmui_can_over_pull, false); pullRate = a.getFloat( R.styleable.QMUIPullLayout_Layout_qmui_pull_rate, pullRate); needReceiveFlingFromTarget = a.getBoolean( R.styleable.QMUIPullLayout_Layout_qmui_need_receive_fling_from_target_view, true); receivedFlingFraction = a.getFloat( R.styleable.QMUIPullLayout_Layout_qmui_received_fling_fraction, receivedFlingFraction); actionInitOffset = a.getDimensionPixelSize(R.styleable.QMUIPullLayout_Layout_qmui_action_view_init_offset, 0); scrollSpeedPerPixel = a.getFloat(R.styleable.QMUIPullLayout_Layout_qmui_scroll_speed_per_pixel, scrollSpeedPerPixel); triggerUntilScrollToTriggerOffset = a.getBoolean(R.styleable.QMUIPullLayout_Layout_qmui_trigger_until_scroll_to_trigger_offset, false); scrollToTriggerOffsetAfterTouchUp = a.getBoolean(R.styleable.QMUIPullLayout_Layout_qmui_scroll_to_trigger_offset_after_touch_up, true); } a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } } public final static class PullAction { @NonNull private final View mActionView; private final int mTargetTriggerOffset; private final boolean mCanOverPull; private final float mPullRate; private final float mReceivedFlingFraction; private final int mActionInitOffset; @PullEdge private final int mPullEdge; private final float mScrollSpeedPerPixel; private final boolean mNeedReceiveFlingFromTargetView; private final boolean mTriggerUntilScrollToTriggerOffset; private final boolean mScrollToTriggerOffsetAfterTouchUp; private final QMUIViewOffsetHelper mViewOffsetHelper; private final ActionViewOffsetCalculator mActionViewOffsetCalculator; private boolean mIsActionRunning = false; PullAction(@NonNull View actionView, int targetOffset, boolean isTargetCanOverPull, float targetPullRate, int actionInitOffset, @PullEdge int pullEdge, float scrollSpeedPerPixel, boolean needReceiveFlingFromTargetView, float receivedFlingFraction, boolean triggerUntilScrollToTriggerOffset, boolean scrollToTriggerOffsetAfterTouchUp, ActionViewOffsetCalculator calculator) { mActionView = actionView; mTargetTriggerOffset = targetOffset; mCanOverPull = isTargetCanOverPull; mPullRate = targetPullRate; mNeedReceiveFlingFromTargetView = needReceiveFlingFromTargetView; mReceivedFlingFraction = receivedFlingFraction; mActionInitOffset = actionInitOffset; mScrollSpeedPerPixel = scrollSpeedPerPixel; mPullEdge = pullEdge; mTriggerUntilScrollToTriggerOffset = triggerUntilScrollToTriggerOffset; mScrollToTriggerOffsetAfterTouchUp = scrollToTriggerOffsetAfterTouchUp; mActionViewOffsetCalculator = calculator; mViewOffsetHelper = new QMUIViewOffsetHelper(actionView); updateOffset(actionInitOffset); } public int getActionPullSize() { if (mPullEdge == PULL_EDGE_TOP || mPullEdge == PULL_EDGE_BOTTOM) { return mActionView.getHeight(); } return mActionView.getWidth(); } public int getActionInitOffset() { return mActionInitOffset; } public int getTargetTriggerOffset() { if (mTargetTriggerOffset == ViewGroup.LayoutParams.WRAP_CONTENT) { return getActionPullSize() - getActionInitOffset() * 2; } return mTargetTriggerOffset; } public float getScrollSpeedPerPixel() { return mScrollSpeedPerPixel; } public float getPullRate() { return mPullRate; } public boolean isNeedReceiveFlingFromTargetView() { return mNeedReceiveFlingFromTargetView; } public boolean isScrollToTriggerOffsetAfterTouchUp() { return mScrollToTriggerOffsetAfterTouchUp; } public boolean isTriggerUntilScrollToTriggerOffset() { return mTriggerUntilScrollToTriggerOffset; } public float getFlingRate(int currentTargetOffset){ return Math.min(mPullRate, Math.max(mPullRate - (currentTargetOffset - getTargetTriggerOffset()) * mReceivedFlingFraction, 0)); } public boolean isCanOverPull() { return mCanOverPull; } @PullEdge public int getPullEdge() { return mPullEdge; } void updateOffset(int offset) { if (mPullEdge == PULL_EDGE_LEFT) { mViewOffsetHelper.setLeftAndRightOffset(offset); } else if (mPullEdge == PULL_EDGE_TOP) { mViewOffsetHelper.setTopAndBottomOffset(offset); } else if (mPullEdge == PULL_EDGE_RIGHT) { mViewOffsetHelper.setLeftAndRightOffset(-offset); } else { mViewOffsetHelper.setTopAndBottomOffset(-offset); } } void onTargetMoved(int targetOffset) { updateOffset( mActionViewOffsetCalculator.calculateOffset(this, targetOffset)); } } public static class PullActionBuilder { @NonNull private final View mActionView; private int mTargetTriggerOffset = ViewGroup.LayoutParams.WRAP_CONTENT; private boolean mCanOverPull; private float mPullRate = DEFAULT_PULL_RATE; private boolean mNeedReceiveFlingFromTargetView = true; private float mReceivedFlingFraction = DEFAULT_FLING_FRACTION; private int mActionInitOffset; private float mScrollSpeedPerPixel = DEFAULT_SCROLL_SPEED_PER_PIXEL; @PullEdge private int mPullEdge; private ActionViewOffsetCalculator mActionViewOffsetCalculator; private boolean mTriggerUntilScrollToTriggerOffset = false; private boolean mScrollToTriggerOffsetAfterTouchUp = true; public PullActionBuilder(@NonNull View actionView, @PullEdge int pullEdge) { mActionView = actionView; mPullEdge = pullEdge; } public PullActionBuilder triggerUntilScrollToTriggerOffset(boolean triggerUntilScrollToTriggerOffset){ mTriggerUntilScrollToTriggerOffset = triggerUntilScrollToTriggerOffset; return this; } public PullActionBuilder scrollToTriggerOffsetAfterTouchUp(boolean scrollToTriggerOffsetAfterTouchUp){ mScrollToTriggerOffsetAfterTouchUp = scrollToTriggerOffsetAfterTouchUp; return this; } public PullActionBuilder targetTriggerOffset(int offset) { mTargetTriggerOffset = offset; return this; } public PullActionBuilder canOverPull(boolean canOverPull) { mCanOverPull = canOverPull; return this; } public PullActionBuilder receivedFlingFraction(float fraction) { mReceivedFlingFraction = fraction; return this; } public PullActionBuilder needReceiveFlingFromTargetView(boolean needReceive) { mNeedReceiveFlingFromTargetView = needReceive; return this; } public PullActionBuilder pullRate(float rate){ mPullRate = rate; return this; } public PullActionBuilder scrollSpeedPerPixel(float scrollSpeedPerPixel){ mScrollSpeedPerPixel = scrollSpeedPerPixel; return this; } public PullActionBuilder actionInitOffset(int initOffset) { mActionInitOffset = initOffset; return this; } public PullActionBuilder actionViewOffsetCalculator(ActionViewOffsetCalculator calculator) { mActionViewOffsetCalculator = calculator; return this; } PullAction build() { if (mActionViewOffsetCalculator == null) { mActionViewOffsetCalculator = new QMUIAlwaysFollowOffsetCalculator(); } return new PullAction(mActionView, mTargetTriggerOffset, mCanOverPull, mPullRate, mActionInitOffset, mPullEdge, mScrollSpeedPerPixel, mNeedReceiveFlingFromTargetView, mReceivedFlingFraction, mTriggerUntilScrollToTriggerOffset, mScrollToTriggerOffsetAfterTouchUp, mActionViewOffsetCalculator); } } public interface ActionViewOffsetCalculator { int calculateOffset(PullAction pullAction, int targetOffset); } public interface ActionPullWatcherView { void onPull(PullAction pullAction, int currentTargetOffset); void onActionTriggered(); void onActionFinished(); } public interface StopTargetViewFlingImpl { void stopFling(View view); } public static class DefaultStopTargetViewFlingImpl implements StopTargetViewFlingImpl { private static DefaultStopTargetViewFlingImpl sInstance; public static DefaultStopTargetViewFlingImpl getInstance() { if (sInstance == null) { sInstance = new DefaultStopTargetViewFlingImpl(); } return sInstance; } private DefaultStopTargetViewFlingImpl() { } @Override public void stopFling(View view) { if (view instanceof RecyclerView) { ((RecyclerView) view).stopScroll(); } } } public interface ActionListener { void onActionTriggered(@NonNull PullAction pullAction); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullLoadMoreView.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.widget.pullLayout; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.collection.SimpleArrayMap; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.widget.ImageViewCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUILoadingView; public class QMUIPullLoadMoreView extends ConstraintLayout implements QMUIPullLayout.ActionPullWatcherView { private boolean mIsLoading = false; private QMUILoadingView mLoadingView; private AppCompatImageView mArrowView; private AppCompatTextView mTextView; private int mHeight; private String mPullText; private String mReleaseText; private boolean mIsInReleaseState = false; public QMUIPullLoadMoreView(Context context) { this(context, null); } public QMUIPullLoadMoreView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUIPullLoadMoreStyle); } public QMUIPullLoadMoreView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIPullLoadMoreView, defStyleAttr, 0); mPullText = array.getString(R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_pull_text); mReleaseText = array.getString(R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_release_text); mHeight = array.getDimensionPixelSize( R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_height, QMUIDisplayHelper.dp2px(context, 56)); int loadSize = array.getDimensionPixelSize( R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_loading_size, QMUIDisplayHelper.dp2px(context, 20)); int textSize = array.getDimensionPixelSize( R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_text_size, QMUIDisplayHelper.sp2px(context, 14)); int arrowTextGap = array.getDimensionPixelSize( R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_arrow_text_gap, QMUIDisplayHelper.dp2px(context, 10)); Drawable arrow = array.getDrawable(R.styleable.QMUIPullLoadMoreView_qmui_pull_load_more_arrow); int bgColor = array.getColor( R.styleable.QMUIPullLoadMoreView_qmui_skin_support_pull_load_more_bg_color, Color.TRANSPARENT); int loadingTintColor = array.getColor( R.styleable.QMUIPullLoadMoreView_qmui_skin_support_pull_load_more_loading_tint_color, Color.BLACK); int arrowTintColor = array.getColor( R.styleable.QMUIPullLoadMoreView_qmui_skin_support_pull_load_more_arrow_tint_color, Color.BLACK); int textColor = array.getColor( R.styleable.QMUIPullLoadMoreView_qmui_skin_support_pull_load_more_text_color, Color.BLACK); array.recycle(); mLoadingView = new QMUILoadingView(context); mLoadingView.setSize(loadSize); mLoadingView.setColor(loadingTintColor); mLoadingView.setVisibility(View.GONE); LayoutParams lp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToLeft = LayoutParams.PARENT_ID; lp.rightToRight = LayoutParams.PARENT_ID; lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; addView(mLoadingView, lp); mArrowView = new AppCompatImageView(context); mArrowView.setId(View.generateViewId()); mArrowView.setImageDrawable(arrow); mArrowView.setRotation(180); ImageViewCompat.setImageTintList(mArrowView, ColorStateList.valueOf(arrowTintColor)); mTextView = new AppCompatTextView(context); mTextView.setId(View.generateViewId()); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); mTextView.setTextColor(textColor); mTextView.setText(mPullText); lp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToLeft = LayoutParams.PARENT_ID; lp.rightToLeft = mTextView.getId(); lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; lp.horizontalChainStyle = LayoutParams.CHAIN_PACKED; addView(mArrowView, lp); lp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftToRight = mArrowView.getId(); lp.rightToRight = LayoutParams.PARENT_ID; lp.topToTop = LayoutParams.PARENT_ID; lp.bottomToBottom = LayoutParams.PARENT_ID; lp.leftMargin = arrowTextGap; addView(mTextView, lp); setBackgroundColor(bgColor); QMUISkinValueBuilder skinValueBuilder = QMUISkinValueBuilder.acquire(); skinValueBuilder.background(R.attr.qmui_skin_support_pull_load_more_bg_color); QMUISkinHelper.setSkinValue(this, skinValueBuilder); skinValueBuilder.clear(); skinValueBuilder.tintColor(R.attr.qmui_skin_support_pull_load_more_loading_tint_color); QMUISkinHelper.setSkinValue(mLoadingView, skinValueBuilder); skinValueBuilder.clear(); skinValueBuilder.tintColor(R.attr.qmui_skin_support_pull_load_more_arrow_tint_color); QMUISkinHelper.setSkinValue(mArrowView, skinValueBuilder); skinValueBuilder.clear(); skinValueBuilder.textColor(R.attr.qmui_skin_support_pull_load_more_text_color); QMUISkinHelper.setSkinValue(mTextView, skinValueBuilder); skinValueBuilder.release(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY)); } @Override public void onPull(QMUIPullLayout.PullAction pullAction, int currentTargetOffset) { if(mIsLoading){ return; } if(mIsInReleaseState){ if(pullAction.getTargetTriggerOffset() > currentTargetOffset){ mIsInReleaseState = false; mTextView.setText(mPullText); mArrowView.animate().rotation(180).start(); } }else{ if(pullAction.getTargetTriggerOffset() <= currentTargetOffset){ mIsInReleaseState = true; mTextView.setText(mReleaseText); mArrowView.animate().rotation(0).start(); } } } @Override public void onActionTriggered() { mIsLoading = true; mLoadingView.setVisibility(View.VISIBLE); mLoadingView.start(); mArrowView.setVisibility(View.GONE); mTextView.setVisibility(View.GONE); } @Override public void onActionFinished() { mIsLoading = false; mLoadingView.stop(); mLoadingView.setVisibility(View.GONE); mArrowView.setVisibility(View.VISIBLE); mTextView.setVisibility(View.VISIBLE); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullLayout/QMUIPullRefreshView.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.widget.pullLayout; import android.content.Context; import android.util.AttributeSet; import android.util.DisplayMetrics; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.appcompat.widget.AppCompatImageView; import androidx.collection.SimpleArrayMap; import androidx.core.content.ContextCompat; import androidx.swiperefreshlayout.widget.CircularProgressDrawable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIResHelper; public class QMUIPullRefreshView extends AppCompatImageView implements QMUIPullLayout.ActionPullWatcherView, IQMUISkinDefaultAttrProvider { private static final int MAX_ALPHA = 255; private static final float TRIM_RATE = 0.85f; private static final float TRIM_OFFSET = 0.4f; static final int CIRCLE_DIAMETER = 40; static final int CIRCLE_DIAMETER_LARGE = 56; private CircularProgressDrawable mProgress; private int mCircleDiameter; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(4); sDefaultSkinAttrs.put(QMUISkinValueBuilder.TINT_COLOR, R.attr.qmui_skin_support_pull_refresh_view_color); } public QMUIPullRefreshView(Context context) { this(context, null); } public QMUIPullRefreshView(Context context, AttributeSet attrs) { super(context, attrs); mProgress = new CircularProgressDrawable(context); setColorSchemeColors(QMUIResHelper.getAttrColor( context, R.attr.qmui_skin_support_pull_refresh_view_color)); mProgress.setStyle(CircularProgressDrawable.LARGE); mProgress.setAlpha(MAX_ALPHA); mProgress.setArrowScale(0.8f); setImageDrawable(mProgress); final DisplayMetrics metrics = getResources().getDisplayMetrics(); mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mCircleDiameter, mCircleDiameter); } @Override public void onActionTriggered() { mProgress.start(); } @Override public void onActionFinished() { mProgress.stop(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mProgress.stop(); } @Override public void onPull(QMUIPullLayout.PullAction pullAction, int currentTargetOffset) { if (mProgress.isRunning()) { return; } int targetOffset = pullAction.getTargetTriggerOffset(); float end = TRIM_RATE * Math.min(targetOffset, currentTargetOffset) / targetOffset; float rotate = TRIM_OFFSET * currentTargetOffset / targetOffset; mProgress.setArrowEnabled(true); mProgress.setStartEndTrim(0, end); mProgress.setProgressRotation(rotate); } public void setSize(@CircularProgressDrawable.ProgressDrawableSize int size) { if (size != CircularProgressDrawable.LARGE && size != CircularProgressDrawable.DEFAULT) { return; } final DisplayMetrics metrics = getResources().getDisplayMetrics(); if (size == CircularProgressDrawable.LARGE) { mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); } else { mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } // force the bounds of the progress circle inside the circle view to // update by setting it to null before updating its size and then // re-setting it setImageDrawable(null); mProgress.setStyle(size); setImageDrawable(mProgress); } public void stop() { mProgress.stop(); } public void doRefresh() { } public void setColorSchemeResources(@ColorRes int... colorResIds) { final Context context = getContext(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); } setColorSchemeColors(colorRes); } public void setColorSchemeColors(@ColorInt int... colors) { mProgress.setColorSchemeColors(colors); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUICenterGravityRefreshOffsetCalculator.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.widget.pullRefreshLayout; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout.RefreshOffsetCalculator; /** * 当targetCurrentOffset < refreshViewHeight, refreshView跟随targetView,其距离为0 * 当targetCurrentOffset >= targetRefreshOffset RefreshView垂直方向永远居中于在[0, targetCurrentOffset] * * @author cginechen * @date 2017-06-07 */ public class QMUICenterGravityRefreshOffsetCalculator implements RefreshOffsetCalculator { @Override public int calculateRefreshOffset(int refreshInitOffset, int refreshEndOffset, int refreshViewHeight, int targetCurrentOffset, int targetInitOffset, int targetRefreshOffset) { if(targetCurrentOffset < refreshViewHeight){ return targetCurrentOffset - refreshViewHeight; } return (targetCurrentOffset - refreshViewHeight) / 2; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIDefaultRefreshOffsetCalculator.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.widget.pullRefreshLayout; /** * {@link QMUIPullRefreshLayout}的默认RefreshView偏移量计算器: * 偏移范围限定在[refreshInitOffset, refreshEndOffset] * * @author cginechen * @date 2017-06-07 */ public class QMUIDefaultRefreshOffsetCalculator implements QMUIPullRefreshLayout.RefreshOffsetCalculator { @Override public int calculateRefreshOffset(int refreshInitOffset, int refreshEndOffset, int refreshViewHeight, int targetCurrentOffset, int targetInitOffset, int targetRefreshOffset) { int refreshOffset; if (targetCurrentOffset >= targetRefreshOffset) { refreshOffset = refreshEndOffset; } else if (targetCurrentOffset <= targetInitOffset) { refreshOffset = refreshInitOffset; } else { float percent = (targetCurrentOffset - targetInitOffset) * 1.0f / (targetRefreshOffset - targetInitOffset); refreshOffset = (int) (refreshInitOffset + percent * (refreshEndOffset - refreshInitOffset)); } return refreshOffset; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIFollowRefreshOffsetCalculator.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.widget.pullRefreshLayout; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout.RefreshOffsetCalculator; /** * RefreshView 永远和 TargetView 保持一定的距离(这个距离由刷新时RefreshView居中算出) * * @author cginechen * @date 2017-06-07 */ public class QMUIFollowRefreshOffsetCalculator implements RefreshOffsetCalculator { @Override public int calculateRefreshOffset(int refreshInitOffset, int refreshEndOffset, int refreshViewHeight, int targetCurrentOffset, int targetInitOffset, int targetRefreshOffset) { int distance = targetRefreshOffset / 2 + refreshViewHeight / 2; int max = targetCurrentOffset - refreshViewHeight; return Math.min(max, targetCurrentOffset - distance); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/pullRefreshLayout/QMUIPullRefreshLayout.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.widget.pullRefreshLayout; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.Scroller; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import androidx.collection.SimpleArrayMap; import androidx.core.content.ContextCompat; import androidx.core.view.NestedScrollingParent; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.CircularProgressDrawable; import com.qmuiteam.qmui.QMUIConfig; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.section.QMUIStickySectionLayout; /** * 下拉刷新控件, 作为容器,下拉时会将子 View 下移, 并拉出 RefreshView(表示正在刷新的 View) *
    *
  • 可通过继承并覆写 {@link #createRefreshView()} 方法实现自己的 RefreshView
  • *
  • 可通过 {@link #setRefreshOffsetCalculator(RefreshOffsetCalculator)} 自己决定在下拉过程中 RefreshView 的位置
  • *
  • 可在 xml 中使用 {@link com.qmuiteam.qmui.R.styleable#QMUIPullRefreshLayout} 这些属性或在 Java 设置对应的属性决定子View的开始位置、触发刷新的位置等值
  • *
* * @author cginechen * @date 2016-12-11 */ public class QMUIPullRefreshLayout extends ViewGroup implements NestedScrollingParent { private static final String TAG = "QMUIPullRefreshLayout"; private static final int INVALID_POINTER = -1; private static final int FLAG_NEED_SCROLL_TO_INIT_POSITION = 1; private static final int FLAG_NEED_SCROLL_TO_REFRESH_POSITION = 1 << 1; private static final int FLAG_NEED_DO_REFRESH = 1 << 2; private static final int FLAG_NEED_DELIVER_VELOCITY = 1 << 3; private final NestedScrollingParentHelper mNestedScrollingParentHelper; boolean mIsRefreshing = false; private View mTargetView; private IRefreshView mIRefreshView; private View mRefreshView; private int mRefreshZIndex = -1; private int mSystemTouchSlop; private int mTouchSlop; private OnPullListener mListener; private OnChildScrollUpCallback mChildScrollUpCallback; /** * RefreshView的初始offset */ private int mRefreshInitOffset; /** * 刷新时RefreshView的offset */ private int mRefreshEndOffset; /** * RefreshView当前offset */ private int mRefreshCurrentOffset; /** * 是否自动根据RefreshView的高度计算RefreshView的初始位置 */ private boolean mAutoCalculateRefreshInitOffset = true; /** * 是否自动根据TargetView在刷新时的位置计算RefreshView的结束位置 */ private boolean mAutoCalculateRefreshEndOffset = true; /** * 自动让TargetView的刷新位置与RefreshView高度相等 */ private boolean mEqualTargetRefreshOffsetToRefreshViewHeight = false; /** * 当拖拽超过超过mAutoScrollToRefreshMinOffset时,自动滚动到刷新位置并触发刷新 * mAutoScrollToRefreshMinOffset == - 1表示要mAutoScrollToRefreshMinOffset>=mTargetRefreshOffset */ private int mAutoScrollToRefreshMinOffset = -1; /** * TargetView(ListView或者ScrollView等)的初始位置 */ private int mTargetInitOffset; /** * 下拉时 TargetView(ListView 或者 ScrollView 等)当前的位置。 */ private int mTargetCurrentOffset; /** * 刷新时TargetView(ListView或者ScrollView等)的位置 */ private int mTargetRefreshOffset; private boolean mDisableNestScrollImpl = false; private boolean mEnableOverPull = true; private boolean mNestedScrollInProgress; private int mActivePointerId = INVALID_POINTER; private boolean mIsDragging; private float mInitialDownY; private float mInitialDownX; @SuppressWarnings("FieldCanBeLocal") private float mInitialMotionY; private float mLastMotionY; private float mDragRate = 0.65f; private RefreshOffsetCalculator mRefreshOffsetCalculator; private VelocityTracker mVelocityTracker; private float mMaxVelocity; private float mMiniVelocity; private Scroller mScroller; private int mScrollFlag = 0; private boolean mNestScrollDurationRefreshing = false; private Runnable mPendingRefreshDirectlyAction = null; private boolean mSafeDisallowInterceptTouchEvent = false; public QMUIPullRefreshLayout(Context context) { this(context, null); } public QMUIPullRefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUIPullRefreshLayoutStyle); } public QMUIPullRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setWillNotDraw(false); final ViewConfiguration vc = ViewConfiguration.get(context); mMaxVelocity = vc.getScaledMaximumFlingVelocity(); mMiniVelocity = vc.getScaledMinimumFlingVelocity(); mSystemTouchSlop = vc.getScaledTouchSlop(); mTouchSlop = QMUIDisplayHelper.px2dp(context, mSystemTouchSlop); //系统的值是8dp,如何配置? mScroller = new Scroller(getContext()); mScroller.setFriction(getScrollerFriction()); addRefreshView(); ViewCompat.setChildrenDrawingOrderEnabled(this, true); mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUIPullRefreshLayout, defStyleAttr, 0); try { mRefreshInitOffset = array.getDimensionPixelSize( R.styleable.QMUIPullRefreshLayout_qmui_refresh_init_offset, Integer.MIN_VALUE); mRefreshEndOffset = array.getDimensionPixelSize( R.styleable.QMUIPullRefreshLayout_qmui_refresh_end_offset, Integer.MIN_VALUE); mTargetInitOffset = array.getDimensionPixelSize( R.styleable.QMUIPullRefreshLayout_qmui_target_init_offset, 0); mTargetRefreshOffset = array.getDimensionPixelSize( R.styleable.QMUIPullRefreshLayout_qmui_target_refresh_offset, QMUIDisplayHelper.dp2px(getContext(), 72)); mAutoCalculateRefreshInitOffset = mRefreshInitOffset == Integer.MIN_VALUE || array.getBoolean(R.styleable.QMUIPullRefreshLayout_qmui_auto_calculate_refresh_init_offset, false); mAutoCalculateRefreshEndOffset = mRefreshEndOffset == Integer.MIN_VALUE || array.getBoolean(R.styleable.QMUIPullRefreshLayout_qmui_auto_calculate_refresh_end_offset, false); mEqualTargetRefreshOffsetToRefreshViewHeight = array.getBoolean(R.styleable.QMUIPullRefreshLayout_qmui_equal_target_refresh_offset_to_refresh_view_height, false); } finally { array.recycle(); } mRefreshCurrentOffset = mRefreshInitOffset; mTargetCurrentOffset = mTargetInitOffset; } public static boolean defaultCanScrollUp(View view) { if (view == null) { return false; } if (view instanceof QMUIContinuousNestedScrollLayout) { QMUIContinuousNestedScrollLayout layout = (QMUIContinuousNestedScrollLayout) view; return layout.getCurrentScroll() > 0; } if (view instanceof QMUIStickySectionLayout) { QMUIStickySectionLayout layout = (QMUIStickySectionLayout) view; return defaultCanScrollUp(layout.getRecyclerView()); } if (android.os.Build.VERSION.SDK_INT < 14) { if (view instanceof AbsListView) { final AbsListView absListView = (AbsListView) view; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) .getTop() < absListView.getPaddingTop()); } else { return ViewCompat.canScrollVertically(view, -1) || view.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(view, -1); } } public void setOnPullListener(OnPullListener listener) { mListener = listener; } public void setDisableNestScrollImpl(boolean disableNestScrollImpl) { mDisableNestScrollImpl = disableNestScrollImpl; } public void setDragRate(float dragRate) { // have no idea to change drag rate for nest scroll mDisableNestScrollImpl = true; mDragRate = dragRate; } public void setChildScrollUpCallback(OnChildScrollUpCallback childScrollUpCallback) { mChildScrollUpCallback = childScrollUpCallback; } protected float getScrollerFriction() { return ViewConfiguration.getScrollFriction(); } public void setAutoScrollToRefreshMinOffset(int autoScrollToRefreshMinOffset) { mAutoScrollToRefreshMinOffset = autoScrollToRefreshMinOffset; } public boolean isRefreshing() { return mIsRefreshing; } /** * 覆盖该方法以实现自己的 RefreshView。 * * @return 自定义的 RefreshView, 注意该 View 必须实现 {@link IRefreshView} 接口 */ protected View createRefreshView() { return new RefreshView(getContext()); } private void addRefreshView() { if (mRefreshView == null) { mRefreshView = createRefreshView(); } if (!(mRefreshView instanceof IRefreshView)) { throw new RuntimeException("refreshView must be a instance of IRefreshView"); } mIRefreshView = (IRefreshView) mRefreshView; if (mRefreshView.getLayoutParams() == null) { mRefreshView.setLayoutParams(new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } addView(mRefreshView); } /** * 设置在下拉过程中 RefreshView 的偏移量 */ public void setRefreshOffsetCalculator(RefreshOffsetCalculator refreshOffsetCalculator) { mRefreshOffsetCalculator = refreshOffsetCalculator; } @Override protected int getChildDrawingOrder(int childCount, int i) { if (mRefreshZIndex < 0) { return i; } // 最后才绘制mRefreshView if (i == mRefreshZIndex) { return childCount - 1; } if (i > mRefreshZIndex) { return i - 1; } return i; } /** * child view call, to ensure disallowInterceptTouchEvent make sense *

* how to optimize this... */ public void openSafeDisallowInterceptTouchEvent() { mSafeDisallowInterceptTouchEvent = true; } @Override public void requestDisallowInterceptTouchEvent(boolean b) { if (mSafeDisallowInterceptTouchEvent) { super.requestDisallowInterceptTouchEvent(b); mSafeDisallowInterceptTouchEvent = false; } // if this is a List < L or another view that doesn't support nested // scrolling, ignore this request so that the vertical scroll event // isn't stolen //noinspection StatementWithEmptyBody if ((android.os.Build.VERSION.SDK_INT < 21 && mTargetView instanceof AbsListView) || (mTargetView != null && !ViewCompat.isNestedScrollingEnabled(mTargetView))) { // Nope. } else { super.requestDisallowInterceptTouchEvent(b); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int targetMeasureWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight() , MeasureSpec.EXACTLY); int targetMeasureHeightSpec = MeasureSpec.makeMeasureSpec(heightSize - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); measureChild(mRefreshView, widthMeasureSpec, heightMeasureSpec); int refreshViewHeight = mRefreshView.getMeasuredHeight(); if (mAutoCalculateRefreshInitOffset) { if (mRefreshInitOffset != -refreshViewHeight) { mRefreshInitOffset = -refreshViewHeight; mRefreshCurrentOffset = mRefreshInitOffset; } } if (mEqualTargetRefreshOffsetToRefreshViewHeight) { mTargetRefreshOffset = refreshViewHeight; } if (mAutoCalculateRefreshEndOffset) { mRefreshEndOffset = (mTargetRefreshOffset - refreshViewHeight) / 2; } mRefreshZIndex = -1; for (int i = 0; i < getChildCount(); i++) { if (getChildAt(i) == mRefreshView) { mRefreshZIndex = i; break; } } ensureTargetView(); if (mTargetView == null) { Log.d(TAG, "onMeasure: mTargetView == null"); setMeasuredDimension(widthSize, heightSize); return; } mTargetView.measure(targetMeasureWidthSpec, targetMeasureHeightSpec); setMeasuredDimension(widthSize, heightSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } ensureTargetView(); if (mTargetView == null) { Log.d(TAG, "onLayout: mTargetView == null"); return; } final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); mTargetView.layout(childLeft, childTop + mTargetCurrentOffset, childLeft + childWidth, childTop + childHeight + mTargetCurrentOffset); int refreshViewWidth = mRefreshView.getMeasuredWidth(); int refreshViewHeight = mRefreshView.getMeasuredHeight(); mRefreshView.layout((width / 2 - refreshViewWidth / 2), mRefreshCurrentOffset, (width / 2 + refreshViewWidth / 2), mRefreshCurrentOffset + refreshViewHeight); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTargetView(); final int action = ev.getAction(); int pointerIndex; if (!isEnabled() || canChildScrollUp() || mNestedScrollInProgress) { if (QMUIConfig.DEBUG) { Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = " + canChildScrollUp() + " ; mNestedScrollInProgress = " + mNestedScrollInProgress); } return false; } switch (action) { case MotionEvent.ACTION_DOWN: mIsDragging = false; mActivePointerId = ev.getPointerId(0); pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } mInitialDownX = ev.getX(pointerIndex); mInitialDownY = ev.getY(pointerIndex); break; case MotionEvent.ACTION_MOVE: pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); startDragging(x, y); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsDragging = false; mActivePointerId = INVALID_POINTER; break; } return mIsDragging; } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); int pointerIndex; if (!isEnabled() || canChildScrollUp() || mNestedScrollInProgress) { Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = " + canChildScrollUp() + " ; mNestedScrollInProgress = " + mNestedScrollInProgress); return false; } acquireVelocityTracker(ev); switch (action) { case MotionEvent.ACTION_DOWN: mIsDragging = false; mScrollFlag = 0; if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mActivePointerId = ev.getPointerId(0); break; case MotionEvent.ACTION_MOVE: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(TAG, "onTouchEvent Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); startDragging(x, y); if (mIsDragging) { float dy = (y - mLastMotionY) * mDragRate; if (dy >= 0) { moveTargetView(dy); } else { int move = moveTargetView(dy); float delta = Math.abs(dy) - Math.abs(move); if (delta > 0) { // 重新dispatch一次down事件,使得列表可以继续滚动 ev.setAction(MotionEvent.ACTION_DOWN); // 立刻dispatch一个大于mSystemTouchSlop的move事件,防止触发TargetView float offsetLoc = mSystemTouchSlop + 1; if (delta > offsetLoc) { offsetLoc = delta; } ev.offsetLocation(0, offsetLoc); super.dispatchTouchEvent(ev); ev.setAction(action); // 再dispatch一次move事件,消耗掉所有dy ev.offsetLocation(0, -offsetLoc); super.dispatchTouchEvent(ev); } } mLastMotionY = y; } break; } case MotionEvent.ACTION_POINTER_DOWN: { pointerIndex = ev.getActionIndex(); if (pointerIndex < 0) { Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = ev.getPointerId(pointerIndex); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } if (mIsDragging) { mIsDragging = false; mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); float vy = mVelocityTracker.getYVelocity(mActivePointerId); if (Math.abs(vy) < mMiniVelocity) { vy = 0; } finishPull((int) vy); } mActivePointerId = INVALID_POINTER; releaseVelocityTracker(); return false; } case MotionEvent.ACTION_CANCEL: releaseVelocityTracker(); return false; } return true; } private void ensureTargetView() { if (mTargetView == null) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); if (!view.equals(mRefreshView)) { onSureTargetView(view); mTargetView = view; break; } } } if (mTargetView != null && mPendingRefreshDirectlyAction != null) { Runnable runnable = mPendingRefreshDirectlyAction; mPendingRefreshDirectlyAction = null; runnable.run(); } } /** * 确定TargetView, 提供机会给子类来做一些初始化的操作 */ protected void onSureTargetView(View targetView) { } protected void onFinishPull(int vy, int refreshInitOffset, int refreshEndOffset, int refreshViewHeight, int targetCurrentOffset, int targetInitOffset, int targetRefreshOffset) { } private void finishPull(int vy) { info("finishPull: vy = " + vy + " ; mTargetCurrentOffset = " + mTargetCurrentOffset + " ; mTargetRefreshOffset = " + mTargetRefreshOffset + " ; mTargetInitOffset = " + mTargetInitOffset + " ; mScroller.isFinished() = " + mScroller.isFinished()); int miniVy = vy / 1000; // 向下拖拽时, 速度不能太大 onFinishPull(miniVy, mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getMeasuredHeight(), mTargetCurrentOffset, mTargetInitOffset, mTargetRefreshOffset); if (mTargetCurrentOffset >= mTargetRefreshOffset) { if (miniVy > 0) { mScrollFlag = FLAG_NEED_SCROLL_TO_REFRESH_POSITION | FLAG_NEED_DO_REFRESH; mScroller.fling(0, mTargetCurrentOffset, 0, miniVy, 0, 0, mTargetInitOffset, Integer.MAX_VALUE); invalidate(); } else if (miniVy < 0) { mScroller.fling(0, mTargetCurrentOffset, 0, vy, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); if (mScroller.getFinalY() < mTargetInitOffset) { mScrollFlag = FLAG_NEED_DELIVER_VELOCITY; } else if (mScroller.getFinalY() < mTargetRefreshOffset) { int dy = mTargetInitOffset - mTargetCurrentOffset; mScroller.startScroll(0, mTargetCurrentOffset, 0, dy); } else if (mScroller.getFinalY() == mTargetRefreshOffset) { mScrollFlag = FLAG_NEED_DO_REFRESH; } else { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); mScrollFlag = FLAG_NEED_DO_REFRESH; } invalidate(); } else { if (mTargetCurrentOffset > mTargetRefreshOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); } mScrollFlag = FLAG_NEED_DO_REFRESH; invalidate(); } } else { if (miniVy > 0) { mScroller.fling(0, mTargetCurrentOffset, 0, miniVy, 0, 0, mTargetInitOffset, Integer.MAX_VALUE); if (mScroller.getFinalY() > mTargetRefreshOffset) { mScrollFlag = FLAG_NEED_SCROLL_TO_REFRESH_POSITION | FLAG_NEED_DO_REFRESH; } else if (mAutoScrollToRefreshMinOffset >= 0 && mScroller.getFinalY() > mAutoScrollToRefreshMinOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); mScrollFlag = FLAG_NEED_DO_REFRESH; } else { mScrollFlag = FLAG_NEED_SCROLL_TO_INIT_POSITION; } invalidate(); } else if (miniVy < 0) { mScrollFlag = 0; mScroller.fling(0, mTargetCurrentOffset, 0, vy, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); if (mScroller.getFinalY() < mTargetInitOffset) { mScrollFlag = FLAG_NEED_DELIVER_VELOCITY; } else { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset); mScrollFlag = 0; } invalidate(); } else { if (mTargetCurrentOffset == mTargetInitOffset) { return; } if (mAutoScrollToRefreshMinOffset >= 0 && mTargetCurrentOffset >= mAutoScrollToRefreshMinOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); mScrollFlag = FLAG_NEED_DO_REFRESH; } else { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset); mScrollFlag = 0; } invalidate(); } } } protected void onRefresh() { if (mIsRefreshing) { return; } mIsRefreshing = true; mIRefreshView.doRefresh(); if (mListener != null) { mListener.onRefresh(); } } public void finishRefresh() { mIsRefreshing = false; mIRefreshView.stop(); mScrollFlag = FLAG_NEED_SCROLL_TO_INIT_POSITION; mScroller.forceFinished(true); invalidate(); } public void setToRefreshDirectly() { setToRefreshDirectly(0, true); } public void setToRefreshDirectly(final long delay){ setToRefreshDirectly(delay, true); } public void setToRefreshDirectly(final long delay, final boolean animate) { if (mTargetView != null) { Runnable runnable = new Runnable() { @Override public void run() { setTargetViewToTop(mTargetView); if(animate){ mScrollFlag = FLAG_NEED_SCROLL_TO_REFRESH_POSITION; invalidate(); }else{ moveTargetViewTo(mTargetRefreshOffset, true); } onRefresh(); } }; if(delay == 0){ runnable.run(); }else{ postDelayed(runnable, delay); } } else { mPendingRefreshDirectlyAction = new Runnable() { @Override public void run() { setToRefreshDirectly(delay, animate); } }; } } public void setEnableOverPull(boolean enableOverPull) { mEnableOverPull = enableOverPull; } protected void setTargetViewToTop(View targetView) { if (targetView instanceof RecyclerView) { ((RecyclerView) targetView).scrollToPosition(0); } else if (targetView instanceof AbsListView) { AbsListView listView = (AbsListView) targetView; listView.setSelectionFromTop(0, 0); } else { targetView.scrollTo(0, 0); } } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = ev.getActionIndex(); final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); } } public void reset() { mIRefreshView.stop(); mIsRefreshing = false; mScroller.forceFinished(true); mScrollFlag = 0; moveTargetViewTo(mTargetInitOffset); } protected void startDragging(float x, float y) { final float dx = x - mInitialDownX; final float dy = y - mInitialDownY; boolean isYDrag = isYDrag(dx, dy); if (isYDrag && (dy > mTouchSlop || (dy < -mTouchSlop && mTargetCurrentOffset > mTargetInitOffset)) && !mIsDragging) { mInitialMotionY = mInitialDownY + mTouchSlop; mLastMotionY = mInitialMotionY; mIsDragging = true; } } protected boolean isYDrag(float dx, float dy) { return Math.abs(dy) > Math.abs(dx); } public boolean isDragging() { return mIsDragging; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!enabled) { reset(); invalidate(); } } public boolean canChildScrollUp() { if (mChildScrollUpCallback != null) { return mChildScrollUpCallback.canChildScrollUp(this, mTargetView); } return defaultCanScrollUp(mTargetView); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { info("onStartNestedScroll: nestedScrollAxes = " + nestedScrollAxes); return !mDisableNestScrollImpl && isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { info("onNestedScrollAccepted: axes = " + axes); mScroller.abortAnimation(); mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); mNestedScrollInProgress = true; mIsDragging = true; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { info("onNestedPreScroll: dx = " + dx + " ; dy = " + dy); int parentCanConsume = mTargetCurrentOffset - mTargetInitOffset; if (dy > 0 && parentCanConsume > 0) { if (dy >= parentCanConsume) { consumed[1] = parentCanConsume; moveTargetViewTo(mTargetInitOffset); } else { consumed[1] = dy; moveTargetView(-dy); } } } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { info("onNestedScroll: dxConsumed = " + dxConsumed + " ; dyConsumed = " + dyConsumed + " ; dxUnconsumed = " + dxUnconsumed + " ; dyUnconsumed = " + dyUnconsumed); if (dyUnconsumed < 0 && !canChildScrollUp() && mScroller.isFinished() && mScrollFlag == 0) { moveTargetView(-dyUnconsumed); } } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } @Override public void onStopNestedScroll(View child) { info("onStopNestedScroll: mNestedScrollInProgress = " + mNestedScrollInProgress); mNestedScrollingParentHelper.onStopNestedScroll(child); if (mNestedScrollInProgress) { mNestedScrollInProgress = false; mIsDragging = false; if (!mNestScrollDurationRefreshing) { finishPull(0); } } } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { info("onNestedPreFling: mTargetCurrentOffset = " + mTargetCurrentOffset + " ; velocityX = " + velocityX + " ; velocityY = " + velocityY); if (mTargetCurrentOffset > mTargetInitOffset) { mNestedScrollInProgress = false; mIsDragging = false; if (!mNestScrollDurationRefreshing) { finishPull((int) -velocityY); } return true; } return false; } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { try { return super.onNestedFling(target, velocityX, velocityY, consumed); } catch (Throwable e) { // android 24及以上ViewGroup会继续往上派发, 23以及以下直接返回false // 低于5.0的机器和RecyclerView配合工作时,部分机型会调用这个方法,但是ViewGroup并没有实现这个方法,会报错,这里catch一下 } return false; } private int moveTargetView(float dy) { int target = (int) (mTargetCurrentOffset + dy); return moveTargetViewTo(target); } private int moveTargetViewTo(int target) { return moveTargetViewTo(target, false); } private int moveTargetViewTo(int target, boolean calculateAnyWay) { target = calculateTargetOffset(target, mTargetInitOffset, mTargetRefreshOffset, mEnableOverPull); int offset = 0; if (target != mTargetCurrentOffset || calculateAnyWay) { offset = target - mTargetCurrentOffset; ViewCompat.offsetTopAndBottom(mTargetView, offset); mTargetCurrentOffset = target; int total = mTargetRefreshOffset - mTargetInitOffset; if (!mIsRefreshing) { mIRefreshView.onPull(Math.min(mTargetCurrentOffset - mTargetInitOffset, total), total, mTargetCurrentOffset - mTargetRefreshOffset); } onMoveTargetView(mTargetCurrentOffset); if (mListener != null) { mListener.onMoveTarget(mTargetCurrentOffset); } if (mRefreshOffsetCalculator == null) { mRefreshOffsetCalculator = new QMUIDefaultRefreshOffsetCalculator(); } int newRefreshOffset = mRefreshOffsetCalculator.calculateRefreshOffset(mRefreshInitOffset, mRefreshEndOffset, mRefreshView.getMeasuredHeight(), mTargetCurrentOffset, mTargetInitOffset, mTargetRefreshOffset); if (newRefreshOffset != mRefreshCurrentOffset) { ViewCompat.offsetTopAndBottom(mRefreshView, newRefreshOffset - mRefreshCurrentOffset); mRefreshCurrentOffset = newRefreshOffset; onMoveRefreshView(mRefreshCurrentOffset); if (mListener != null) { mListener.onMoveRefreshView(mRefreshCurrentOffset); } } } return offset; } protected int calculateTargetOffset(int target, int targetInitOffset, int targetRefreshOffset, boolean enableOverPull) { target = Math.max(target, targetInitOffset); if (!enableOverPull) { target = Math.min(target, targetRefreshOffset); } return target; } private void acquireVelocityTracker(final MotionEvent event) { if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } private void releaseVelocityTracker() { if (null != mVelocityTracker) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } public int getRefreshInitOffset() { return mRefreshInitOffset; } public int getRefreshEndOffset() { return mRefreshEndOffset; } public int getTargetInitOffset() { return mTargetInitOffset; } public int getTargetRefreshOffset() { return mTargetRefreshOffset; } public void setTargetRefreshOffset(int targetRefreshOffset) { mEqualTargetRefreshOffsetToRefreshViewHeight = false; mTargetRefreshOffset = targetRefreshOffset; } public View getTargetView() { return mTargetView; } protected void onMoveTargetView(int offset) { } protected void onMoveRefreshView(int offset) { } private boolean hasFlag(int flag) { return (mScrollFlag & flag) == flag; } private void removeFlag(int flag) { mScrollFlag = mScrollFlag & ~flag; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int offsetY = mScroller.getCurrY(); moveTargetViewTo(offsetY); if (offsetY <= 0 && hasFlag(FLAG_NEED_DELIVER_VELOCITY)) { deliverVelocity(); mScroller.forceFinished(true); } invalidate(); } else if (hasFlag(FLAG_NEED_SCROLL_TO_INIT_POSITION)) { removeFlag(FLAG_NEED_SCROLL_TO_INIT_POSITION); if (mTargetCurrentOffset != mTargetInitOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset); } invalidate(); } else if (hasFlag(FLAG_NEED_SCROLL_TO_REFRESH_POSITION)) { removeFlag(FLAG_NEED_SCROLL_TO_REFRESH_POSITION); if (mTargetCurrentOffset != mTargetRefreshOffset) { mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); } else { moveTargetViewTo(mTargetRefreshOffset, true); } invalidate(); } else if (hasFlag(FLAG_NEED_DO_REFRESH)) { removeFlag(FLAG_NEED_DO_REFRESH); onRefresh(); moveTargetViewTo(mTargetRefreshOffset, true); } else { deliverVelocity(); } } private void deliverVelocity() { if (hasFlag(FLAG_NEED_DELIVER_VELOCITY)) { removeFlag(FLAG_NEED_DELIVER_VELOCITY); if (mScroller.getCurrVelocity() > mMiniVelocity) { info("deliver velocity: " + mScroller.getCurrVelocity()); // if there is a velocity, pass it on if (mTargetView instanceof RecyclerView) { ((RecyclerView) mTargetView).fling(0, (int) mScroller.getCurrVelocity()); } else if (mTargetView instanceof AbsListView && android.os.Build.VERSION.SDK_INT >= 21) { ((AbsListView) mTargetView).fling((int) mScroller.getCurrVelocity()); } } } } private void info(String msg) { if (QMUIConfig.DEBUG) { Log.i(TAG, msg); } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { mNestScrollDurationRefreshing = mIsRefreshing || (mScrollFlag & FLAG_NEED_DO_REFRESH) != 0; } else if (mNestScrollDurationRefreshing) { if (action == MotionEvent.ACTION_MOVE) { if (!mIsRefreshing && mScroller.isFinished() && mScrollFlag == 0) { // 这里必须要 dispatch 一次 down 事件,否则不能触发 NestScroll,具体可参考 RecyclerView // down 过程中会触发 onStopNestedScroll,mNestScrollDurationRefreshing 必须在之后 // 置为false,否则会触发 finishPull ev.offsetLocation(0, -mSystemTouchSlop - 1); ev.setAction(MotionEvent.ACTION_DOWN); super.dispatchTouchEvent(ev); mNestScrollDurationRefreshing = false; ev.setAction(action); // offset touch slop, 避免触发点击事件 ev.offsetLocation(0, mSystemTouchSlop + 1); } } else { mNestScrollDurationRefreshing = false; } } return super.dispatchTouchEvent(ev); } public interface OnPullListener { void onMoveTarget(int offset); void onMoveRefreshView(int offset); void onRefresh(); } public interface OnChildScrollUpCallback { boolean canChildScrollUp(QMUIPullRefreshLayout parent, @Nullable View child); } public interface RefreshOffsetCalculator { /** * 通过 targetView 的当前位置、targetView 的初始和刷新位置以及 refreshView 的初始与结束位置计算 RefreshView 的位置。 * * @param refreshInitOffset RefreshView 的初始 offset。 * @param refreshEndOffset 刷新时 RefreshView 的 offset。 * @param refreshViewHeight RerfreshView 的高度 * @param targetCurrentOffset 下拉时 TargetView(ListView 或者 ScrollView 等)当前的位置。 * @param targetInitOffset TargetView(ListView 或者 ScrollView 等)的初始位置。 * @param targetRefreshOffset 刷新时 TargetView(ListView 或者 ScrollView等)的位置。 * @return RefreshView 当前的位置。 */ int calculateRefreshOffset(int refreshInitOffset, int refreshEndOffset, int refreshViewHeight, int targetCurrentOffset, int targetInitOffset, int targetRefreshOffset); } public interface IRefreshView { void stop(); void doRefresh(); void onPull(int offset, int total, int overPull); } public static class RefreshView extends AppCompatImageView implements IRefreshView, IQMUISkinDefaultAttrProvider { private static final int MAX_ALPHA = 255; private static final float TRIM_RATE = 0.85f; private static final float TRIM_OFFSET = 0.4f; static final int CIRCLE_DIAMETER = 40; static final int CIRCLE_DIAMETER_LARGE = 56; private CircularProgressDrawable mProgress; private int mCircleDiameter; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(4); sDefaultSkinAttrs.put(QMUISkinValueBuilder.TINT_COLOR, R.attr.qmui_skin_support_pull_refresh_view_color); } public RefreshView(Context context) { super(context); mProgress = new CircularProgressDrawable(context); setColorSchemeColors(QMUIResHelper.getAttrColor( context, R.attr.qmui_skin_support_pull_refresh_view_color)); mProgress.setStyle(CircularProgressDrawable.LARGE); mProgress.setAlpha(MAX_ALPHA); mProgress.setArrowScale(0.8f); setImageDrawable(mProgress); final DisplayMetrics metrics = getResources().getDisplayMetrics(); mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mCircleDiameter, mCircleDiameter); } @Override public void onPull(int offset, int total, int overPull) { if (mProgress.isRunning()) { return; } float end = TRIM_RATE * offset / total; float rotate = TRIM_OFFSET * offset / total; if (overPull > 0) { rotate += TRIM_OFFSET * overPull / total; } mProgress.setArrowEnabled(true); mProgress.setStartEndTrim(0, end); mProgress.setProgressRotation(rotate); } public void setSize(@CircularProgressDrawable.ProgressDrawableSize int size) { if (size != CircularProgressDrawable.LARGE && size != CircularProgressDrawable.DEFAULT) { return; } final DisplayMetrics metrics = getResources().getDisplayMetrics(); if (size == CircularProgressDrawable.LARGE) { mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); } else { mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } // force the bounds of the progress circle inside the circle view to // update by setting it to null before updating its size and then // re-setting it setImageDrawable(null); mProgress.setStyle(size); setImageDrawable(mProgress); } public void stop() { mProgress.stop(); } public void doRefresh() { mProgress.start(); } public void setColorSchemeResources(@ColorRes int... colorResIds) { final Context context = getContext(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); } setColorSchemeColors(colorRes); } public void setColorSchemeColors(@ColorInt int... colors) { mProgress.setColorSchemeColors(colors); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButton.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.widget.roundwidget; import android.content.Context; import android.content.res.ColorStateList; import android.util.AttributeSet; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaButton; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIViewHelper; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; /** * 使按钮能方便地指定圆角、边框颜色、边框粗细、背景色 *

* 注意: 因为该控件的圆角采用 View 的 background 实现, 所以与原生的 android:background 有冲突。 *

    *
  • 如果在 xml 中用 android:background 指定 background, 该 background 不会生效。
  • *
  • 如果在该 View 构造完后用 {@link #setBackgroundResource(int)} 等方法设置背景, 该背景将覆盖圆角效果。
  • *
*

*

* 如需在 xml 中指定圆角、边框颜色、边框粗细、背景色等值,采用 xml 属性 {@link com.qmuiteam.qmui.R.styleable#QMUIRoundButton} *

*

* 如需在 Java 中指定以上属性, 需要通过 {@link #getBackground()} 获取 {@link QMUIRoundButtonDrawable} 对象, * 然后使用 {@link QMUIRoundButtonDrawable} 提供的方法进行设置。 *

*

* * @see QMUIRoundButtonDrawable *

*/ public class QMUIRoundButton extends QMUIAlphaButton implements IQMUISkinDefaultAttrProvider { private QMUIRoundButtonDrawable mRoundBg; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(3); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_round_btn_bg_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BORDER, R.attr.qmui_skin_support_round_btn_border_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_round_btn_text_color); } public QMUIRoundButton(Context context) { super(context); init(context, null, 0); } public QMUIRoundButton(Context context, AttributeSet attrs) { super(context, attrs, R.attr.QMUIButtonStyle); init(context, attrs, R.attr.QMUIButtonStyle); } public QMUIRoundButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { mRoundBg = QMUIRoundButtonDrawable.fromAttributeSet(context, attrs, defStyleAttr); QMUIViewHelper.setBackgroundKeepingPadding(this, mRoundBg); setChangeAlphaWhenDisable(false); setChangeAlphaWhenPress(false); } @Override public void setBackgroundColor(int color) { mRoundBg.setBgData(ColorStateList.valueOf(color)); } public void setBgData(@Nullable ColorStateList colors) { mRoundBg.setBgData(colors); } public void setStrokeData(int width, @Nullable ColorStateList colors) { mRoundBg.setStrokeData(width, colors); } public int getStrokeWidth(){ return mRoundBg.getStrokeWidth(); } public void setStrokeColors(ColorStateList colors) { mRoundBg.setStrokeColors(colors); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundButtonDrawable.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.widget.roundwidget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.util.AttributeSet; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; /** * 可以方便地生成圆角矩形/圆形 {@link android.graphics.drawable.Drawable}。 *

*

    *
  • 使用 {@link #setBgData(ColorStateList)} 设置背景色。
  • *
  • 使用 {@link #setStrokeData(int, ColorStateList)} 设置描边大小、描边颜色。
  • *
  • 使用 {@link #setIsRadiusAdjustBounds(boolean)} 设置圆角大小是否自动适应为 {@link android.view.View} 的高度的一半, 默认为 true。
  • *
*/ public class QMUIRoundButtonDrawable extends GradientDrawable { /** * 圆角大小是否自适应为 View 的高度的一般 */ private boolean mRadiusAdjustBounds = true; private ColorStateList mFillColors; private int mStrokeWidth = 0; private ColorStateList mStrokeColors; /** * 设置按钮的背景色(只支持纯色,不支持 Bitmap 或 Drawable) */ public void setBgData(@Nullable ColorStateList colors) { super.setColor(colors); } /** * 设置按钮的描边粗细和颜色 */ public void setStrokeData(int width, @Nullable ColorStateList colors) { mStrokeWidth = width; mStrokeColors = colors; super.setStroke(width, colors); } public int getStrokeWidth(){ return mStrokeWidth; } public void setStrokeColors(@Nullable ColorStateList colors){ setStrokeData(mStrokeWidth, colors); } /** * 设置圆角大小是否自动适应为 View 的高度的一半 */ public void setIsRadiusAdjustBounds(boolean isRadiusAdjustBounds) { mRadiusAdjustBounds = isRadiusAdjustBounds; } @Override protected boolean onStateChange(int[] stateSet) { boolean superRet = super.onStateChange(stateSet); if (mFillColors != null) { int color = mFillColors.getColorForState(stateSet, 0); setColor(color); superRet = true; } if (mStrokeColors != null) { int color = mStrokeColors.getColorForState(stateSet, 0); setStroke(mStrokeWidth, color); superRet = true; } return superRet; } @Override public boolean isStateful() { return (mFillColors != null && mFillColors.isStateful()) || (mStrokeColors != null && mStrokeColors.isStateful()) || super.isStateful(); } @Override protected void onBoundsChange(Rect r) { super.onBoundsChange(r); if (mRadiusAdjustBounds) { // 修改圆角为短边的一半 setCornerRadius(Math.min(r.width(), r.height()) / 2); } } public static QMUIRoundButtonDrawable fromAttributeSet(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.QMUIRoundButton, defStyleAttr, 0); ColorStateList colorBg = typedArray.getColorStateList(R.styleable.QMUIRoundButton_qmui_backgroundColor); ColorStateList colorBorder = typedArray.getColorStateList(R.styleable.QMUIRoundButton_qmui_borderColor); int borderWidth = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_borderWidth, 0); boolean isRadiusAdjustBounds = typedArray.getBoolean(R.styleable.QMUIRoundButton_qmui_isRadiusAdjustBounds, false); int mRadius = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_radius, 0); int mRadiusTopLeft = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_radiusTopLeft, 0); int mRadiusTopRight = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_radiusTopRight, 0); int mRadiusBottomLeft = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_radiusBottomLeft, 0); int mRadiusBottomRight = typedArray.getDimensionPixelSize(R.styleable.QMUIRoundButton_qmui_radiusBottomRight, 0); typedArray.recycle(); QMUIRoundButtonDrawable bg = new QMUIRoundButtonDrawable(); bg.setBgData(colorBg); bg.setStrokeData(borderWidth, colorBorder); if (mRadiusTopLeft > 0 || mRadiusTopRight > 0 || mRadiusBottomLeft > 0 || mRadiusBottomRight > 0) { float[] radii = new float[]{ mRadiusTopLeft, mRadiusTopLeft, mRadiusTopRight, mRadiusTopRight, mRadiusBottomRight, mRadiusBottomRight, mRadiusBottomLeft, mRadiusBottomLeft }; bg.setCornerRadii(radii); isRadiusAdjustBounds = false; } else { bg.setCornerRadius(mRadius); if(mRadius > 0){ isRadiusAdjustBounds = false; } } bg.setIsRadiusAdjustBounds(isRadiusAdjustBounds); return bg; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundFrameLayout.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.widget.roundwidget; import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; import com.qmuiteam.qmui.util.QMUIViewHelper; /** * 见 {@link QMUIRoundButton} 与 {@link QMUIRoundButtonDrawable} */ public class QMUIRoundFrameLayout extends FrameLayout { public QMUIRoundFrameLayout(Context context) { super(context); init(context, null, 0); } public QMUIRoundFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0); } public QMUIRoundFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { QMUIRoundButtonDrawable bg = QMUIRoundButtonDrawable.fromAttributeSet(context, attrs, defStyleAttr); QMUIViewHelper.setBackgroundKeepingPadding(this, bg); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundLinearLayout.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.widget.roundwidget; import android.content.Context; import android.util.AttributeSet; import android.widget.LinearLayout; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIViewHelper; /** * 见 {@link QMUIRoundButton} 与 {@link QMUIRoundButtonDrawable} */ public class QMUIRoundLinearLayout extends LinearLayout { public QMUIRoundLinearLayout(Context context) { super(context); init(context, null, 0); } public QMUIRoundLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, R.attr.QMUIButtonStyle); } public QMUIRoundLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { QMUIRoundButtonDrawable bg = QMUIRoundButtonDrawable.fromAttributeSet(context, attrs, 0); QMUIViewHelper.setBackgroundKeepingPadding(this, bg); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/roundwidget/QMUIRoundRelativeLayout.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.widget.roundwidget; import android.content.Context; import android.util.AttributeSet; import android.widget.RelativeLayout; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIViewHelper; /** * 见 {@link QMUIRoundButton} 与 {@link QMUIRoundButtonDrawable} */ public class QMUIRoundRelativeLayout extends RelativeLayout { public QMUIRoundRelativeLayout(Context context) { super(context); init(context, null, 0); } public QMUIRoundRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, R.attr.QMUIButtonStyle); } public QMUIRoundRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { QMUIRoundButtonDrawable bg = QMUIRoundButtonDrawable.fromAttributeSet(context, attrs, defStyleAttr); QMUIViewHelper.setBackgroundKeepingPadding(this, bg); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIDefaultStickySectionAdapter.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.widget.section; import androidx.annotation.NonNull; import android.view.View; import android.view.ViewGroup; public abstract class QMUIDefaultStickySectionAdapter< H extends QMUISection.Model, T extends QMUISection.Model> extends QMUIStickySectionAdapter { public QMUIDefaultStickySectionAdapter() { } public QMUIDefaultStickySectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { super(removeSectionTitleIfOnlyOneSection); } @NonNull @Override protected ViewHolder onCreateSectionLoadingViewHolder(@NonNull ViewGroup viewGroup) { return new ViewHolder(new View(viewGroup.getContext())); } @NonNull @Override protected ViewHolder onCreateCustomItemViewHolder(@NonNull ViewGroup viewGroup, int type) { return new ViewHolder(new View(viewGroup.getContext())); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISection.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.widget.section; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; public class QMUISection, T extends QMUISection.Model> { public static final int SECTION_INDEX_UNKNOWN = -1; public static final int ITEM_INDEX_UNKNOWN = -1; public static final int ITEM_INDEX_SECTION_HEADER = -2; public static final int ITEM_INDEX_LOAD_BEFORE = -3; public static final int ITEM_INDEX_LOAD_AFTER = -4; /** * if add internal index, we should update this item */ public static final int ITEM_INDEX_INTERNAL_END = -4; /** * offset custom index to reduce conflict with internal index */ public static final int ITEM_INDEX_CUSTOM_OFFSET = -1000; private H mHeader; private ArrayList mItemList; private boolean mIsFold; private boolean mIsLocked; private boolean mExistBeforeDataToLoad; private boolean mExistAfterDataToLoad; private boolean mIsErrorToLoadBefore = false; private boolean mIsErrorToLoadAfter = false; public QMUISection(@NonNull H header, @Nullable List itemList) { this(header, itemList, false); } public QMUISection(@NonNull H header, @Nullable List itemList, boolean isFold) { this(header, itemList, isFold, false, false, false); } public QMUISection(@NonNull H header, @Nullable List itemList, boolean isFold, boolean isLocked, boolean existBeforeDataToLoad, boolean existAfterDataToLoad) { mHeader = header; mItemList = new ArrayList<>(); if (itemList != null) { mItemList.addAll(itemList); } mIsFold = isFold; mIsLocked = isLocked; mExistBeforeDataToLoad = existBeforeDataToLoad; mExistAfterDataToLoad = existAfterDataToLoad; } public H getHeader() { return mHeader; } public boolean isFold() { return mIsFold; } public void setFold(boolean fold) { mIsFold = fold; } public boolean isLocked() { return mIsLocked; } public void setLocked(boolean locked) { mIsLocked = locked; } public boolean isExistBeforeDataToLoad() { return mExistBeforeDataToLoad; } public void setExistBeforeDataToLoad(boolean existBeforeDataToLoad) { mExistBeforeDataToLoad = existBeforeDataToLoad; } public boolean isExistAfterDataToLoad() { return mExistAfterDataToLoad; } public void setExistAfterDataToLoad(boolean existAfterDataToLoad) { mExistAfterDataToLoad = existAfterDataToLoad; } public boolean isErrorToLoadBefore() { return mIsErrorToLoadBefore; } public void setErrorToLoadBefore(boolean errorToLoadBefore) { mIsErrorToLoadBefore = errorToLoadBefore; } public boolean isErrorToLoadAfter() { return mIsErrorToLoadAfter; } public void setErrorToLoadAfter(boolean errorToLoadAfter) { mIsErrorToLoadAfter = errorToLoadAfter; } public int getItemCount() { return mItemList.size(); } public T getItemAt(int index){ if (index < 0 || index >= mItemList.size()) { return null; } return mItemList.get(index); } public boolean existItem(T item){ return mItemList.contains(item); } public void finishLoadMore(@Nullable List data, boolean isLoadBefore, boolean existMoreData){ if(isLoadBefore){ if(data != null){ mItemList.addAll(0, data); } mExistBeforeDataToLoad = existMoreData; }else{ if(data != null){ mItemList.addAll(data); } mExistAfterDataToLoad = existMoreData; } } public void cloneStatusTo(QMUISection other) { other.mExistBeforeDataToLoad = mExistBeforeDataToLoad; other.mExistAfterDataToLoad = mExistAfterDataToLoad; other.mIsFold = mIsFold; other.mIsLocked = mIsLocked; other.mIsErrorToLoadBefore = mIsErrorToLoadBefore; other.mIsErrorToLoadAfter = mIsErrorToLoadAfter; } public QMUISection mutate(){ QMUISection section = new QMUISection<>(mHeader, mItemList, mIsFold, mIsLocked, mExistBeforeDataToLoad, mExistAfterDataToLoad); section.mIsErrorToLoadBefore = mIsErrorToLoadBefore; section.mIsErrorToLoadAfter = mIsErrorToLoadAfter; return section; } public QMUISection cloneForDiff() { ArrayList newList = new ArrayList<>(); for (T item : mItemList) { newList.add(item.cloneForDiff()); } QMUISection section = new QMUISection<>(mHeader.cloneForDiff(), newList, mIsFold, mIsLocked, mExistBeforeDataToLoad, mExistAfterDataToLoad); section.mIsErrorToLoadBefore = mIsErrorToLoadBefore; section.mIsErrorToLoadAfter = mIsErrorToLoadAfter; return section; } public static final boolean isCustomItemIndex(int index){ return index < ITEM_INDEX_INTERNAL_END; } public interface Model { /** * Called by QMUISection to clone this model for next diff if the adapter data is mutable. * you just need clone the fields needed for diff * * @return another instance of T */ T cloneForDiff(); /** * Called by QMUIDiffCallback decide whether two object represent the same T. * For example, if your items have unique ids, this method should check their id equality. * * @param other the object to compare * @return True if the two items represent the same object or false if they are different. */ boolean isSameItem(T other); /** * Called by the QMUIDiffCallback when it wants to check whether two items have the same data. * QMUIDiffCallback uses this information to detect if the contents of an item has changed. * * @param other the object to compare * @return True if the contents of the items are the same or false if they are different. */ boolean isSameContent(T other); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUISectionDiffCallback.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.widget.section; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import android.util.SparseIntArray; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_LOAD_AFTER; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_LOAD_BEFORE; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_SECTION_HEADER; public class QMUISectionDiffCallback, T extends QMUISection.Model> extends DiffUtil.Callback { private ArrayList> mOldList = new ArrayList<>(); private ArrayList> mNewList = new ArrayList<>(); private ArrayList mOldSectionIndex = new ArrayList<>(); private ArrayList mOldItemIndex = new ArrayList<>(); private ArrayList mNewSectionIndex = new ArrayList<>(); private ArrayList mNewItemIndex = new ArrayList<>(); private boolean mRemoveSectionTitleIfOnlyOnceSection; public QMUISectionDiffCallback( @Nullable List> oldList, @Nullable List> newList) { if (oldList != null) { mOldList.addAll(oldList); } if (newList != null) { mNewList.addAll(newList); } } void generateIndex(boolean removeSectionTitleIfOnlyOnceSection){ mRemoveSectionTitleIfOnlyOnceSection = removeSectionTitleIfOnlyOnceSection; generateIndex(mOldList, mOldSectionIndex, mOldItemIndex, removeSectionTitleIfOnlyOnceSection); generateIndex(mNewList, mNewSectionIndex, mNewItemIndex, removeSectionTitleIfOnlyOnceSection); } public void cloneNewIndexTo(@NonNull ArrayList sectionIndex, @NonNull ArrayList itemIndex) { sectionIndex.clear(); itemIndex.clear(); sectionIndex.ensureCapacity(mNewSectionIndex.size()); itemIndex.ensureCapacity(mNewItemIndex.size()); for (int i = 0; i < mNewSectionIndex.size(); i++) { sectionIndex.add(i, mNewSectionIndex.get(i)); } for (int i = 0; i < mNewItemIndex.size(); i++) { itemIndex.add(i, mNewItemIndex.get(i)); } } private void generateIndex(List> list, ArrayList sectionIndex, ArrayList itemIndex, boolean removeSectionTitleIfOnlyOnceSection) { sectionIndex.clear(); itemIndex.clear(); IndexGenerationInfo generationInfo = new IndexGenerationInfo(sectionIndex, itemIndex); if (list.isEmpty() || !list.get(0).isLocked()) { onGenerateCustomIndexBeforeSectionList(generationInfo, list); } for (int i = 0; i < list.size(); i++) { QMUISection section = list.get(i); if (section.isLocked()) { continue; } if(!removeSectionTitleIfOnlyOnceSection || list.size() > 1){ generationInfo.appendIndex(i, ITEM_INDEX_SECTION_HEADER); } if (section.isFold()) { continue; } onGenerateCustomIndexBeforeItemList(generationInfo, section, i); if (section.isExistBeforeDataToLoad()) { generationInfo.appendIndex(i, ITEM_INDEX_LOAD_BEFORE); } for (int j = 0; j < section.getItemCount(); j++) { generationInfo.appendIndex(i, j); } if (section.isExistAfterDataToLoad()) { generationInfo.appendIndex(i, ITEM_INDEX_LOAD_AFTER); } onGenerateCustomIndexAfterItemList(generationInfo, section, i); } if (list.isEmpty()) { onGenerateCustomIndexAfterSectionList(generationInfo, list); } else { QMUISection lastSection = list.get(list.size() - 1); if (!lastSection.isLocked() && (lastSection.isFold() || !lastSection.isExistAfterDataToLoad())) { onGenerateCustomIndexAfterSectionList(generationInfo, list); } } } /** * Subclasses overrides this method to add custom view before the beginning of the list, such as list header. * Use {@link IndexGenerationInfo#appendWholeListCustomIndex(int)} to add index info * * @param generationInfo call generationInfo.appendWholeListCustomIndex to collect index info * @param list the whole list info */ protected void onGenerateCustomIndexBeforeSectionList(IndexGenerationInfo generationInfo, List> list) { } /** * Subclasses overrides this method to add custom view after the end of the list, such as list footer. * Use {@link IndexGenerationInfo#appendWholeListCustomIndex(int)} to add index info * * @param generationInfo call generationInfo.appendWholeListCustomIndex to collect index info * @param list the whole list info */ protected void onGenerateCustomIndexAfterSectionList(IndexGenerationInfo generationInfo, List> list) { } /** * Subclasses overrides this method to add custom view before the beginning of the section content list * Use {@link IndexGenerationInfo#appendCustomIndex(int, int)} to add index info * * @param generationInfo call generationInfo.appendIndex to collect index info * @param section section info * @param sectionIndex section index info */ protected void onGenerateCustomIndexBeforeItemList(IndexGenerationInfo generationInfo, QMUISection section, int sectionIndex) { } /** * Subclasses overrides this method to add custom view before the end of the section content list * Use {@link IndexGenerationInfo#appendIndex(int, int)} to add index info * * @param generationInfo call generationInfo.appendCustomIndex to collect index info * @param section section info * @param sectionIndex section index info */ protected void onGenerateCustomIndexAfterItemList(IndexGenerationInfo generationInfo, QMUISection section, int sectionIndex) { } /** * Subclasses overrides this method to check whether two custom items have the same data * @param oldSection the old section in the old list * @param oldItemIndex the old item index in old section * @param newSection the new section in the new list * @param newItemIndex the new item index in new section * @return True if the contents of the items are the same or false if they are different. */ protected boolean areCustomContentsTheSame(@Nullable QMUISection oldSection, int oldItemIndex, @Nullable QMUISection newSection, int newItemIndex) { return false; } @Override public int getOldListSize() { return mOldSectionIndex.size(); } @Override public int getNewListSize() { return mNewSectionIndex.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { int oldSectionIndex = mOldSectionIndex.get(oldItemPosition); int oldItemIndex = mOldItemIndex.get(oldItemPosition); int newSectionIndex = mNewSectionIndex.get(newItemPosition); int newItemIndex = mNewItemIndex.get(newItemPosition); if (oldSectionIndex < 0 || newSectionIndex < 0) { return oldSectionIndex == newSectionIndex && oldItemIndex == newItemIndex; } QMUISection oldModel = mOldList.get(oldSectionIndex); QMUISection newModel = mNewList.get(newSectionIndex); if (!oldModel.getHeader().isSameItem(newModel.getHeader())) { return false; } if (oldItemIndex < 0 && oldItemIndex == newItemIndex) { return true; } if (oldItemIndex < 0 || newItemIndex < 0) { return false; } T oldItem = oldModel.getItemAt(oldItemIndex); T newItem = newModel.getItemAt(newItemIndex); return (oldItem == null && newItem == null) || (oldItem != null && newItem != null && oldItem.isSameItem(newItem)); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { int oldSectionIndex = mOldSectionIndex.get(oldItemPosition); int oldItemIndex = mOldItemIndex.get(oldItemPosition); int newSectionIndex = mNewSectionIndex.get(newItemPosition); int newItemIndex = mNewItemIndex.get(newItemPosition); if (newSectionIndex < 0) { return areCustomContentsTheSame(null, oldItemIndex, null, newItemIndex); } if(mRemoveSectionTitleIfOnlyOnceSection){ // may be the indentation is changed. if(mOldList.size() == 1 && mNewList.size() != 1){ return false; } if(mOldList.size() != 1 && mNewList.size() == 1){ return false; } } QMUISection oldModel = mOldList.get(oldSectionIndex); QMUISection newModel = mNewList.get(newSectionIndex); if (oldItemIndex == ITEM_INDEX_SECTION_HEADER) { return oldModel.isFold() == newModel.isFold() && oldModel.getHeader().isSameContent(newModel.getHeader()); } if (oldItemIndex == ITEM_INDEX_LOAD_BEFORE || oldItemIndex == ITEM_INDEX_LOAD_AFTER) { // forced to return false,so we can trigger to load more // in QMUIStickySectionAdapter.onViewAttachedToWindow return false; } if (QMUISection.isCustomItemIndex(oldItemIndex)) { return areCustomContentsTheSame(oldModel, oldItemIndex, newModel, newItemIndex); } T oldItem = oldModel.getItemAt(oldItemIndex); T newItem = newModel.getItemAt(newItemIndex); return (oldItem == null && newItem == null) || (oldItem != null && newItem != null && oldItem.isSameContent(newItem)); } public static class IndexGenerationInfo { private ArrayList sectionIndexArray; private ArrayList itemIndexArray; private IndexGenerationInfo(ArrayList sectionIndex, ArrayList itemIndex) { sectionIndexArray = sectionIndex; itemIndexArray = itemIndex; } public final void appendCustomIndex(int sectionIndex, int itemIndex) { int offset = QMUISection.ITEM_INDEX_CUSTOM_OFFSET + itemIndex; if(!QMUISection.isCustomItemIndex(offset)){ throw new IllegalArgumentException( "Index conflicts with index used internally, please use negative number for custom item"); } appendIndex(sectionIndex, offset); } private void appendIndex(int sectionIndex, int itemIndex) { if (sectionIndex < 0) { throw new IllegalArgumentException("use appendWholeListCustomIndex for whole list"); } sectionIndexArray.add(sectionIndex); itemIndexArray.add(itemIndex); } public final void appendWholeListCustomIndex(int itemIndex) { int offset = QMUISection.ITEM_INDEX_CUSTOM_OFFSET + itemIndex; if(!QMUISection.isCustomItemIndex(offset)){ throw new IllegalArgumentException( "Index conflicts with index used internally, please use negative number for custom item"); } appendWholeListIndex(offset); } private void appendWholeListIndex(int itemIndex) { sectionIndexArray.add(QMUISection.SECTION_INDEX_UNKNOWN); itemIndexArray.add(itemIndex); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionAdapter.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.widget.section; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_CUSTOM_OFFSET; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_LOAD_AFTER; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_LOAD_BEFORE; import static com.qmuiteam.qmui.widget.section.QMUISection.ITEM_INDEX_SECTION_HEADER; public abstract class QMUIStickySectionAdapter< H extends QMUISection.Model, T extends QMUISection.Model, VH extends QMUIStickySectionAdapter.ViewHolder> extends RecyclerView.Adapter { private static final String TAG = "StickySectionAdapter"; public static final int ITEM_TYPE_UNKNOWN = -1; public static final int ITEM_TYPE_SECTION_HEADER = 0; public static final int ITEM_TYPE_SECTION_ITEM = 1; public static final int ITEM_TYPE_SECTION_LOADING = 2; public static final int ITEM_TYPE_CUSTOM_OFFSET = 1000; private List> mBackupData = new ArrayList<>(); private List> mCurrentData = new ArrayList<>(); private ArrayList mSectionIndex = new ArrayList<>(); private ArrayList mItemIndex = new ArrayList<>(); private ArrayList> mLoadingBeforeSections = new ArrayList<>(2); private ArrayList> mLoadingAfterSections = new ArrayList<>(2); private Callback mCallback; private ViewCallback mViewCallback; private final boolean mRemoveSectionTitleIfOnlyOneSection; public QMUIStickySectionAdapter() { this(false); } public QMUIStickySectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { mRemoveSectionTitleIfOnlyOneSection = removeSectionTitleIfOnlyOneSection; } /** * see {@link #setData(List, boolean, boolean)} * * @param data section list */ public final void setData(@Nullable List> data) { setData(data, true); } /** * see {@link #setData(List, boolean, boolean)} * * @param data section list * @param onlyMutateState This is used to backup for next diff. True to use shallow copy, false tp use deep copy. */ public final void setData(@Nullable List> data, boolean onlyMutateState){ setData(data, onlyMutateState, true); } /** * set the new data to the adapter, this will trigger diff between new data and old data. * you should pay attention to the state of your data in memory. if new data and old data * reference to the same data in memory, the diff will fail. This is why the parameter * onlyMutateState exists: * if onlyMutateState == true, shallow copy is used to backup for next diff. You must sure H, T in memory is * different between old data and new data * if onlyMutateState == false, deep copy is used to backup for next diff. It's safe, but it will consume * unnecessary performance if your new data is different in memory. * * @param data section list * @param onlyMutateState This is used to backup for next diff. True to use shallow copy, false tp use deep copy. * @param checkLock check section lock */ public final void setData(@Nullable List> data, boolean onlyMutateState, boolean checkLock) { mLoadingBeforeSections.clear(); mLoadingAfterSections.clear(); mCurrentData.clear(); if (data != null) { mCurrentData.addAll(data); } beforeDiffInSet(mBackupData, mCurrentData); if(!mCurrentData.isEmpty() && checkLock){ lock(mCurrentData.get(0)); } diff(true, onlyMutateState); } /** * Subclasses override this method to fill some info to new section list if need. * For example, assume the user expand some section by click event, these action while * modify old section list, but the new section list knows nothing for user action. * so this method is a chance to synchronize some info from old section list. * * @param oldData old section list * @param newData new section list */ protected void beforeDiffInSet(List> oldData, List> newData) { } /** * * @param data section list * @param onlyMutateState this is used to backup for next diff. True to use shallow copy, false tp use deep copy. */ public final void setDataWithoutDiff(@Nullable List> data, boolean onlyMutateState){ setDataWithoutDiff(data, onlyMutateState, true); } /** * same as {@link #setData(List, boolean)}, but do't use {@link DiffUtil}, * use {@link #notifyDataSetChanged()} directly. * * @param data section list * @param onlyMutateState this is used to backup for next diff. True to use shallow copy, false tp use deep copy. * @param checkLock check section lock */ public final void setDataWithoutDiff(@Nullable List> data, boolean onlyMutateState, boolean checkLock) { mLoadingBeforeSections.clear(); mLoadingAfterSections.clear(); mCurrentData.clear(); if (data != null) { mCurrentData.addAll(data); } if(checkLock && !mCurrentData.isEmpty()){ lock(mCurrentData.get(0)); } // only used to generate index info QMUISectionDiffCallback callback = createDiffCallback(mBackupData, mCurrentData); callback.generateIndex(mRemoveSectionTitleIfOnlyOneSection); callback.cloneNewIndexTo(mSectionIndex, mItemIndex); notifyDataSetChanged(); mBackupData.clear(); for (QMUISection section : mCurrentData) { mBackupData.add(onlyMutateState ? section.mutate() : section.cloneForDiff()); } } private void diff(boolean newDataSet, boolean onlyMutateState) { QMUISectionDiffCallback callback = createDiffCallback(mBackupData, mCurrentData); callback.generateIndex(mRemoveSectionTitleIfOnlyOneSection); DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback, false); callback.cloneNewIndexTo(mSectionIndex, mItemIndex); diffResult.dispatchUpdatesTo(this); if (newDataSet || mBackupData.size() != mCurrentData.size()) { mBackupData.clear(); for (QMUISection section : mCurrentData) { mBackupData.add(onlyMutateState ? section.mutate() : section.cloneForDiff()); } } else { //only status change, so we only copy statuses to mBackupData for (int i = 0; i < mCurrentData.size(); i++) { mCurrentData.get(i).cloneStatusTo(mBackupData.get(i)); } } } /** * section data is not changed, only custom item index may changed, so we also need to regenerate index */ public void refreshCustomData() { QMUISectionDiffCallback callback = createDiffCallback(mBackupData, mCurrentData); callback.generateIndex(mRemoveSectionTitleIfOnlyOneSection); DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback, false); callback.cloneNewIndexTo(mSectionIndex, mItemIndex); diffResult.dispatchUpdatesTo(this); } protected QMUISectionDiffCallback createDiffCallback( List> lastData, List> currentData) { return new QMUISectionDiffCallback<>(lastData, currentData); } public void setCallback(Callback callback) { mCallback = callback; } void setViewCallback(ViewCallback viewCallback) { mViewCallback = viewCallback; } public int getSectionCount() { return mCurrentData.size(); } public int getItemIndex(int position) { if (position < 0 || position >= mItemIndex.size()) { return QMUISection.ITEM_INDEX_UNKNOWN; } return mItemIndex.get(position); } public int getSectionIndex(int position) { if (position < 0 || position >= mSectionIndex.size()) { return QMUISection.SECTION_INDEX_UNKNOWN; } return mSectionIndex.get(position); } @Nullable public QMUISection getSection(int position) { if (position < 0 || position >= mSectionIndex.size()) { return null; } int sectionIndex = mSectionIndex.get(position); if (sectionIndex < 0 || sectionIndex >= mCurrentData.size()) { return null; } return mCurrentData.get(sectionIndex); } @Nullable public QMUISection getSectionDirectly(int index) { if (index < 0 || index >= mCurrentData.size()) { return null; } return mCurrentData.get(index); } public boolean isSectionFold(int position) { QMUISection section = getSection(position); if (section == null) { return false; } return section.isFold(); } @Nullable public T getSectionItem(int position) { int itemIndex = getItemIndex(position); if (itemIndex < 0) { return null; } QMUISection section = getSection(position); if (section == null) { return null; } return section.getItemAt(itemIndex); } public void finishLoadMore(QMUISection section, List itemList, boolean isLoadBefore, boolean existMoreData) { if (isLoadBefore) { mLoadingBeforeSections.remove(section); } else { mLoadingAfterSections.remove(section); } if (!mCurrentData.contains(section)) { return; } // if load before, we should focus first item in section. otherwise the new data will // wash current items down if (isLoadBefore && !section.isFold()) { for (int i = 0; i < mItemIndex.size(); i++) { int itemIndex = mItemIndex.get(i); if (itemIndex == 0 && section == getSection(i)) { RecyclerView.ViewHolder focusViewHolder = mViewCallback == null ? null : mViewCallback.findViewHolderForAdapterPosition(i); if (focusViewHolder != null) { mViewCallback.requestChildFocus(focusViewHolder.itemView); } break; } } } section.finishLoadMore(itemList, isLoadBefore, existMoreData); lock(section); diff(true, true); } /** * lock section if needed, so we can stop scroll when in loadMore * * @param section */ private void lock(QMUISection section) { boolean lockPrevious = !section.isFold() && section.isExistBeforeDataToLoad() && !section.isErrorToLoadBefore(); boolean lockAfter = !section.isFold() && section.isExistAfterDataToLoad() && !section.isErrorToLoadAfter(); int index = mCurrentData.indexOf(section); if (index < 0 || index >= mCurrentData.size()) { return; } section.setLocked(false); lockBefore(index - 1, lockPrevious); lockAfter(index + 1, lockAfter); } private void lockBefore(int current, boolean needLock) { while (current >= 0) { QMUISection section = mCurrentData.get(current); if (needLock) { section.setLocked(true); } else { section.setLocked(false); needLock = !section.isFold() && section.isExistBeforeDataToLoad() && !section.isErrorToLoadBefore(); } current--; } } private void lockAfter(int current, boolean needLock) { while (current < mCurrentData.size()) { QMUISection section = mCurrentData.get(current); if (needLock) { section.setLocked(true); } else { section.setLocked(false); needLock = !section.isFold() && section.isExistAfterDataToLoad() && !section.isErrorToLoadAfter(); } current++; } } /** * scroll to special section header * * @param targetSection * @param scrollToTop True to scroll to recyclerView Top, false to scroll to visible area. */ public void scrollToSectionHeader(@NonNull QMUISection targetSection, boolean scrollToTop) { if (mViewCallback == null) { return; } for (int i = 0; i < mCurrentData.size(); i++) { QMUISection section = mCurrentData.get(i); if (targetSection.getHeader().isSameItem(section.getHeader())) { if (section.isLocked()) { lock(section); diff(false, true); safeScrollToSection(section, scrollToTop); } else { safeScrollToSection(section, scrollToTop); } return; } } } private void safeScrollToSection(@NonNull QMUISection targetSection, boolean scrollToTop) { for (int i = 0; i < mSectionIndex.size(); i++) { int sectionIndex = mSectionIndex.get(i); if (sectionIndex < 0 || sectionIndex >= mCurrentData.size()) { continue; } int itemIndex = mItemIndex.get(i); if (itemIndex == ITEM_INDEX_SECTION_HEADER) { QMUISection temp = mCurrentData.get(sectionIndex); if (temp.getHeader().isSameItem(targetSection.getHeader())) { mViewCallback.scrollToPosition(i, true, scrollToTop); return; } } } } /** * scroll to special section item * * @param targetSection section info. if your items are not repeated in different section, * you can use null for this method. * @param targetItem item info * @param scrollToTop True to scroll to recyclerView Top, false to scroll to visible area. */ public void scrollToSectionItem(@Nullable QMUISection targetSection, @NonNull T targetItem, boolean scrollToTop) { if (mViewCallback == null) { return; } // can not trust mItemIndex, maybe the section owned this item is folded // if this happened, we should unfold the section for (int i = 0; i < mCurrentData.size(); i++) { QMUISection section = mCurrentData.get(i); if ((targetSection == null && section.existItem(targetItem)) || targetSection == section) { if (section.isFold() || section.isLocked()) { // unlock this section section.setFold(false); lock(section); diff(false, true); safeScrollToSectionItem(section, targetItem, scrollToTop); } else { safeScrollToSectionItem(section, targetItem, scrollToTop); } return; } } } private void safeScrollToSectionItem(@NonNull QMUISection targetSection, @NonNull T item, boolean scrollToTop) { for (int i = 0; i < mItemIndex.size(); i++) { int itemIndex = mItemIndex.get(i); if (itemIndex < 0) { continue; } QMUISection section = getSection(i); if (section != targetSection) { continue; } if (section.getItemAt(itemIndex).isSameItem(item)) { mViewCallback.scrollToPosition(i, false, scrollToTop); return; } } } /** * only for custom item * * @param sectionIndex * @param customItemIndex * @param unFoldTargetSection * @return */ public int findCustomPosition(int sectionIndex, int customItemIndex, boolean unFoldTargetSection) { int itemIndex = QMUISection.ITEM_INDEX_CUSTOM_OFFSET + customItemIndex; return findPosition(sectionIndex, itemIndex, unFoldTargetSection); } /** * find position by sectionIndex and itemIndex * * @param sectionIndex * @param itemIndex * @param unFoldTargetSection * @return */ public int findPosition(int sectionIndex, int itemIndex, boolean unFoldTargetSection) { if (unFoldTargetSection && sectionIndex >= 0) { QMUISection section = mCurrentData.get(sectionIndex); if (section != null && section.isFold()) { section.setFold(false); lock(section); diff(false, true); } } for (int i = 0; i < getItemCount(); i++) { if (mSectionIndex.get(i) != sectionIndex) { continue; } if (mItemIndex.get(i) == itemIndex) { return i; } } return RecyclerView.NO_POSITION; } /** * find position by positionFinder * * @param positionFinder * @param unFoldTargetSection * @return */ public int findPosition(PositionFinder positionFinder, boolean unFoldTargetSection) { if (!unFoldTargetSection) { for (int i = 0; i < getItemCount(); i++) { QMUISection section = getSection(i); if (section == null) { continue; } int itemIndex = getItemIndex(i); if (itemIndex == ITEM_INDEX_SECTION_HEADER) { if (positionFinder.find(section, null)) { return i; } } else if (itemIndex >= 0) { if (positionFinder.find(section, section.getItemAt(itemIndex))) { return i; } } } return RecyclerView.NO_POSITION; } QMUISection targetSection = null; T targetItem = null; loop: for (int i = 0; i < mCurrentData.size(); i++) { QMUISection section = mCurrentData.get(i); if (positionFinder.find(section, null)) { targetSection = section; break; } for (int j = 0; j < section.getItemCount(); j++) { if (positionFinder.find(section, section.getItemAt(j))) { targetSection = section; targetItem = section.getItemAt(j); boolean isFold = section.isFold(); if (isFold) { section.setFold(false); lock(section); diff(false, true); } break loop; } } } for (int i = 0; i < getItemCount(); i++) { QMUISection section = getSection(i); if (section != targetSection) { continue; } int itemIndex = getItemIndex(i); if (itemIndex == ITEM_INDEX_SECTION_HEADER && targetItem == null) { return i; } else if (itemIndex >= 0) { if (section.getItemAt(itemIndex).isSameItem(targetItem)) { return i; } } } return RecyclerView.NO_POSITION; } public void toggleFold(int position, boolean scrollToTop) { QMUISection section = getSection(position); if (section == null) { return; } section.setFold(!section.isFold()); lock(section); diff(false, true); if (scrollToTop && !section.isFold() && mViewCallback != null) { for (int i = 0; i < mSectionIndex.size(); i++) { int itemIndex = getItemIndex(i); if (itemIndex == ITEM_INDEX_SECTION_HEADER && getSection(i) == section) { mViewCallback.scrollToPosition(i, true, true); return; } } } } public int getRelativeStickyPosition(int position) { while (getItemViewType(position) != ITEM_TYPE_SECTION_HEADER) { position--; if (position < 0) { return RecyclerView.NO_POSITION; } } return position; } public boolean isRemoveSectionTitleIfOnlyOneSection() { return mRemoveSectionTitleIfOnlyOneSection; } @Override public final int getItemCount() { return mItemIndex.size(); } @NonNull @Override public final VH onCreateViewHolder(@NonNull ViewGroup viewGroup, int type) { if (type == ITEM_TYPE_SECTION_HEADER) { return onCreateSectionHeaderViewHolder(viewGroup); } else if (type == ITEM_TYPE_SECTION_ITEM) { return onCreateSectionItemViewHolder(viewGroup); } else if (type == ITEM_TYPE_SECTION_LOADING) { return onCreateSectionLoadingViewHolder(viewGroup); } else { return onCreateCustomItemViewHolder(viewGroup, type - ITEM_TYPE_CUSTOM_OFFSET); } } @Override public final void onBindViewHolder(@NonNull final VH vh, int position) { final int stickyPosition = position; QMUISection section = getSection(position); int itemIndex = getItemIndex(position); if (itemIndex == ITEM_INDEX_SECTION_HEADER) { onBindSectionHeader(vh, position, section); } else if (itemIndex >= 0) { onBindSectionItem(vh, position, section, itemIndex); } else if (itemIndex == ITEM_INDEX_LOAD_BEFORE || itemIndex == ITEM_INDEX_LOAD_AFTER) { onBindSectionLoadingItem(vh, position, section, itemIndex == ITEM_INDEX_LOAD_BEFORE); } else { onBindCustomItem(vh, position, section, itemIndex - QMUISection.ITEM_INDEX_CUSTOM_OFFSET); } if (itemIndex == ITEM_INDEX_LOAD_AFTER) { vh.isLoadBefore = false; } else if (itemIndex == ITEM_INDEX_LOAD_BEFORE) { vh.isLoadBefore = true; } vh.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = vh.isForStickyHeader ? stickyPosition : vh.getAdapterPosition(); if (pos != RecyclerView.NO_POSITION && mCallback != null) { mCallback.onItemClick(vh, pos); } } }); vh.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { int pos = vh.isForStickyHeader ? stickyPosition : vh.getAdapterPosition(); if (pos != RecyclerView.NO_POSITION && mCallback != null) { return mCallback.onItemLongClick(vh, pos); } return false; } }); } @NonNull protected abstract VH onCreateSectionHeaderViewHolder(@NonNull ViewGroup viewGroup); @NonNull protected abstract VH onCreateSectionItemViewHolder(@NonNull ViewGroup viewGroup); @NonNull protected abstract VH onCreateSectionLoadingViewHolder(@NonNull ViewGroup viewGroup); @NonNull protected abstract VH onCreateCustomItemViewHolder(@NonNull ViewGroup viewGroup, int type); protected void onBindSectionHeader(VH holder, int position, QMUISection section) { } protected void onBindSectionItem(VH holder, int position, QMUISection section, int itemIndex) { } protected void onBindSectionLoadingItem(VH holder, int position, QMUISection section, boolean loadingBefore) { } protected void onBindCustomItem(VH holder, int position, @Nullable QMUISection section, int itemIndex) { } @Override public final int getItemViewType(int position) { int itemIndex = getItemIndex(position); if (itemIndex == QMUISection.ITEM_INDEX_UNKNOWN) { // QMUIStickySectionItemDecoration uses findFirstVisibleItemPosition to get the layout position // it may be exceed the adapter position range if layout is not updated in time Log.e(TAG, "the item index is undefined, you may need to check your data if not called by QMUIStickySectionItemDecoration."); return ITEM_TYPE_UNKNOWN; } if (itemIndex == ITEM_INDEX_SECTION_HEADER) { return ITEM_TYPE_SECTION_HEADER; } else if (itemIndex == ITEM_INDEX_LOAD_BEFORE || itemIndex == ITEM_INDEX_LOAD_AFTER) { return ITEM_TYPE_SECTION_LOADING; } else if (itemIndex >= 0) { return ITEM_TYPE_SECTION_ITEM; } else { return ITEM_TYPE_CUSTOM_OFFSET + getCustomItemViewType(itemIndex - ITEM_INDEX_CUSTOM_OFFSET, position); } } @Override public void onViewAttachedToWindow(@NonNull VH holder) { if (holder.getItemViewType() == ITEM_TYPE_SECTION_LOADING && mCallback != null) { if (!holder.isLoadError) { QMUISection section = getSection(holder.getAdapterPosition()); if (section != null) { if (holder.isLoadBefore) { if (mLoadingBeforeSections.contains(section)) { return; } mLoadingBeforeSections.add(section); mCallback.loadMore(section, true); } else { if (mLoadingAfterSections.contains(section)) { return; } mLoadingAfterSections.add(section); mCallback.loadMore(section, false); } } } } } protected int getCustomItemViewType(int itemIndex, int position) { return ITEM_TYPE_UNKNOWN; } public interface Callback, T extends QMUISection.Model> { void loadMore(QMUISection section, boolean loadMoreBefore); void onItemClick(ViewHolder holder, int position); boolean onItemLongClick(ViewHolder holder, int position); } public interface ViewCallback { void scrollToPosition(int position, boolean isSectionHeader, boolean scrollToTop); @Nullable RecyclerView.ViewHolder findViewHolderForAdapterPosition(int position); void requestChildFocus(View view); } public interface PositionFinder, T extends QMUISection.Model> { /** * if item == null, indicate this call for header. * * @param section * @param item * @return */ boolean find(@NonNull QMUISection section, @Nullable T item); } public static class ViewHolder extends RecyclerView.ViewHolder { public boolean isLoadError = false; public boolean isLoadBefore = false; public boolean isForStickyHeader = false; public ViewHolder(View itemView) { super(itemView); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionItemDecoration.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.widget.section; import android.graphics.Canvas; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.lang.ref.WeakReference; /** * Created by cgspine on 2018/1/20. */ public class QMUIStickySectionItemDecoration extends RecyclerView.ItemDecoration { private Callback mCallback; private VH mStickyHeaderViewHolder; private int mStickyHeaderViewPosition = RecyclerView.NO_POSITION; private WeakReference mWeakSectionContainer; private int mTargetTop = 0; public QMUIStickySectionItemDecoration(ViewGroup sectionContainer, @NonNull Callback callback) { mCallback = callback; mWeakSectionContainer = new WeakReference<>(sectionContainer); mCallback.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { super.onChanged(); mStickyHeaderViewPosition = RecyclerView.NO_POSITION; mCallback.invalidate(); } @Override public void onItemRangeInserted(int positionStart, int itemCount) { super.onItemRangeInserted(positionStart, itemCount); if (positionStart <= mStickyHeaderViewPosition) { mStickyHeaderViewPosition = RecyclerView.NO_POSITION; mCallback.invalidate(); } } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { super.onItemRangeMoved(fromPosition, toPosition, itemCount); if (fromPosition == mStickyHeaderViewPosition || toPosition == mStickyHeaderViewPosition) { mStickyHeaderViewPosition = RecyclerView.NO_POSITION; mCallback.invalidate(); } } @Override public void onItemRangeChanged(int positionStart, int itemCount) { // stickyViewHolder should update when the adapter updates relative view holder super.onItemRangeChanged(positionStart, itemCount); if (mStickyHeaderViewPosition >= positionStart && mStickyHeaderViewPosition < positionStart + itemCount && mStickyHeaderViewHolder != null && mWeakSectionContainer.get() != null) { mStickyHeaderViewPosition = RecyclerView.NO_POSITION; mCallback.invalidate(); } } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { super.onItemRangeRemoved(positionStart, itemCount); if (mStickyHeaderViewPosition >= positionStart && mStickyHeaderViewPosition < positionStart + itemCount) { mStickyHeaderViewPosition = RecyclerView.NO_POSITION; setHeaderVisibility(false); } } }); } private void setHeaderVisibility(boolean visibility) { ViewGroup sectionContainer = mWeakSectionContainer.get(); if (sectionContainer == null) { return; } sectionContainer.setVisibility(visibility ? View.VISIBLE : View.GONE); mCallback.onHeaderVisibilityChanged(visibility); } public int getStickyHeaderViewPosition() { return mStickyHeaderViewPosition; } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { ViewGroup sectionContainer = mWeakSectionContainer.get(); if (sectionContainer == null) { return; } if(parent.getChildCount() == 0){ setHeaderVisibility(false); } RecyclerView.Adapter adapter = parent.getAdapter(); if (adapter == null) { setHeaderVisibility(false); return; } RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (!(layoutManager instanceof LinearLayoutManager)) { setHeaderVisibility(false); return; } LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition(); if (firstVisibleItemPosition == RecyclerView.NO_POSITION) { setHeaderVisibility(false); return; } int headerPos = mCallback.getRelativeStickyItemPosition(firstVisibleItemPosition); if (headerPos == RecyclerView.NO_POSITION) { setHeaderVisibility(false); return; } int itemType = mCallback.getItemViewType(headerPos); if (itemType == QMUIStickySectionAdapter.ITEM_TYPE_UNKNOWN) { setHeaderVisibility(false); return; } if (mStickyHeaderViewHolder == null || mStickyHeaderViewHolder.getItemViewType() != itemType) { mStickyHeaderViewHolder = createStickyViewHolder(parent, headerPos, itemType); } if (mStickyHeaderViewPosition != headerPos) { mStickyHeaderViewPosition = headerPos; bindStickyViewHolder(sectionContainer, mStickyHeaderViewHolder, headerPos); } setHeaderVisibility(true); int contactPoint = sectionContainer.getHeight() - 1; final View childInContact = parent.findChildViewUnder(parent.getWidth() / 2, contactPoint); if (childInContact == null) { mTargetTop = parent.getTop(); ViewCompat.offsetTopAndBottom(sectionContainer, mTargetTop - sectionContainer.getTop()); return; } if (mCallback.isHeaderItem(parent.getChildAdapterPosition(childInContact))) { mTargetTop = childInContact.getTop() + parent.getTop() - sectionContainer.getHeight(); ViewCompat.offsetTopAndBottom(sectionContainer, mTargetTop - sectionContainer.getTop()); return; } mTargetTop = parent.getTop(); ViewCompat.offsetTopAndBottom(sectionContainer, mTargetTop - sectionContainer.getTop()); } public int getTargetTop() { return mTargetTop; } private VH createStickyViewHolder(RecyclerView recyclerView, int position, int itemType) { VH vh = mCallback.createViewHolder(recyclerView, itemType); vh.isForStickyHeader = true; return vh; } private void bindStickyViewHolder(ViewGroup sectionContainer, VH viewHolder, int position) { mCallback.bindViewHolder(viewHolder, position); sectionContainer.removeAllViews(); sectionContainer.addView(viewHolder.itemView); } public interface Callback { /** * @param pos adapterPosition * @return sticky section header position */ int getRelativeStickyItemPosition(int pos); boolean isHeaderItem(int pos); ViewHolder createViewHolder(ViewGroup parent, int viewType); void bindViewHolder(ViewHolder holder, int position); int getItemViewType(int position); void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer); void onHeaderVisibilityChanged(boolean visible); void invalidate(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/section/QMUIStickySectionLayout.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.widget.section; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import java.util.ArrayList; import java.util.List; public class QMUIStickySectionLayout extends QMUIFrameLayout implements QMUIStickySectionAdapter.ViewCallback { private RecyclerView mRecyclerView; private QMUIFrameLayout mStickySectionWrapView; private QMUIStickySectionItemDecoration mStickySectionItemDecoration; private int mStickySectionViewHeight = -1; private List mDrawDecorations; /** * if scrollToPosition happened before mStickySectionWrapView finished layout, * the target item may be covered by mStickySectionWrapView, so we delay to * execute the scroll action */ private Runnable mPendingScrollAction = null; public QMUIStickySectionLayout(Context context) { this(context, null); } public QMUIStickySectionLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUIStickySectionLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mStickySectionWrapView = new QMUIFrameLayout(context); mRecyclerView = new RecyclerView(context); addView(mRecyclerView, new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); addView(mStickySectionWrapView, new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mStickySectionWrapView.addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { mStickySectionViewHeight = bottom - top; if (mStickySectionViewHeight > 0 && mPendingScrollAction != null) { mPendingScrollAction.run(); mPendingScrollAction = null; } } }); } public void addDrawDecoration(@NonNull DrawDecoration drawDecoration){ if(mDrawDecorations == null){ mDrawDecorations = new ArrayList<>(); } mDrawDecorations.add(drawDecoration); } public void removeDrawDecoration(@NonNull DrawDecoration drawDecoration){ if(mDrawDecorations == null || mDrawDecorations.isEmpty()){ return; } mDrawDecorations.remove(drawDecoration); } public void configStickySectionWrapView(StickySectionWrapViewConfig stickySectionWrapViewConfig) { if (stickySectionWrapViewConfig != null) { stickySectionWrapViewConfig.config(mStickySectionWrapView); } } public QMUIFrameLayout getStickySectionWrapView() { return mStickySectionWrapView; } public RecyclerView getRecyclerView() { return mRecyclerView; } public @Nullable View getStickySectionView() { if (mStickySectionWrapView.getVisibility() != View.VISIBLE || mStickySectionWrapView.getChildCount() == 0) { return null; } return mStickySectionWrapView.getChildAt(0); } public int getStickyHeaderPosition() { if (mStickySectionItemDecoration == null) { return RecyclerView.NO_POSITION; } return mStickySectionItemDecoration.getStickyHeaderViewPosition(); } /** * proxy to {@link RecyclerView#setLayoutManager(RecyclerView.LayoutManager)} * * @param layoutManager LayoutManager to use */ public void setLayoutManager(@NonNull RecyclerView.LayoutManager layoutManager) { mRecyclerView.setLayoutManager(layoutManager); } /** * section header will be sticky when scrolling, see {@link #setAdapter(QMUIStickySectionAdapter, boolean)} * * @param adapter the adapter inherited from QMUIStickySectionAdapter * @param generic parameter of QMUIStickySectionAdapter, indicating the section header * @param generic parameter of QMUIStickySectionAdapter, indicating the section item * @param generic parameter of QMUIStickySectionAdapter, indicating the view holder */ public , T extends QMUISection.Model, VH extends QMUIStickySectionAdapter.ViewHolder> void setAdapter( QMUIStickySectionAdapter adapter) { setAdapter(adapter, true); } /** * set the adapter for recyclerView, the parameter sticky indicates whether * the section header is sticky or not when scrolling. * * @param adapter the adapter inherited from QMUIStickySectionAdapter * @param sticky if true, make the section header sticky when scrolling * @param generic parameter of QMUIStickySectionAdapter, indicating the section header * @param generic parameter of QMUIStickySectionAdapter, indicating the section item * @param generic parameter of QMUIStickySectionAdapter, indicating the view holder */ public , T extends QMUISection.Model, VH extends QMUIStickySectionAdapter.ViewHolder> void setAdapter( final QMUIStickySectionAdapter adapter, boolean sticky) { if (sticky) { QMUIStickySectionItemDecoration.Callback callback = new QMUIStickySectionItemDecoration.Callback() { @Override public int getRelativeStickyItemPosition(int pos) { return adapter.getRelativeStickyPosition(pos); } @Override public boolean isHeaderItem(int pos) { return adapter.getItemViewType(pos) == QMUIStickySectionAdapter.ITEM_TYPE_SECTION_HEADER; } @Override public VH createViewHolder(ViewGroup parent, int viewType) { return adapter.createViewHolder(parent, viewType); } @Override public void bindViewHolder(VH holder, int position) { adapter.bindViewHolder(holder, position); } @Override public int getItemViewType(int position) { return adapter.getItemViewType(position); } @Override public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { adapter.registerAdapterDataObserver(observer); } @Override public void onHeaderVisibilityChanged(boolean visible) { } @Override public void invalidate() { mRecyclerView.invalidate(); } }; mStickySectionItemDecoration = new QMUIStickySectionItemDecoration<>(mStickySectionWrapView, callback); mRecyclerView.addItemDecoration(mStickySectionItemDecoration); } adapter.setViewCallback(this); mRecyclerView.setAdapter(adapter); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mStickySectionItemDecoration != null) { mStickySectionWrapView.layout(mStickySectionWrapView.getLeft(), mStickySectionItemDecoration.getTargetTop(), mStickySectionWrapView.getRight(), mStickySectionItemDecoration.getTargetTop() + mStickySectionWrapView.getHeight()); } } @Override public void scrollToPosition(final int position, boolean isSectionHeader, final boolean scrollToTop) { mPendingScrollAction = null; RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter == null || position < 0 || position >= adapter.getItemCount()) { return; } RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; int firstVPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition(); int lastVPos = linearLayoutManager.findLastCompletelyVisibleItemPosition(); int offset = 0; if (!isSectionHeader) { if (mStickySectionViewHeight <= 0) { // delay to re scroll mPendingScrollAction = new Runnable() { @Override public void run() { scrollToPosition(position, false, scrollToTop); } }; } offset = mStickySectionWrapView.getHeight(); } if (position < firstVPos + 1 /* increase one to avoid being covered */ || position > lastVPos || scrollToTop) { linearLayoutManager.scrollToPositionWithOffset(position, offset); } } else { mRecyclerView.scrollToPosition(position); } } @Nullable @Override public RecyclerView.ViewHolder findViewHolderForAdapterPosition(int position) { return mRecyclerView.findViewHolderForAdapterPosition(position); } @Override public void requestChildFocus(View view) { mRecyclerView.requestChildFocus(view, null); } public interface StickySectionWrapViewConfig { void config(QMUIFrameLayout stickySectionWrapView); } @Override protected void dispatchDraw(Canvas canvas) { if(mDrawDecorations != null){ for(DrawDecoration drawDecoration: mDrawDecorations){ drawDecoration.onDraw(canvas, this); } } super.dispatchDraw(canvas); if(mDrawDecorations != null){ for(DrawDecoration drawDecoration: mDrawDecorations){ drawDecoration.onDrawOver(canvas, this); } } } @Override public void onDescendantInvalidated(@NonNull View child, @NonNull View target) { super.onDescendantInvalidated(child, target); if(target == mRecyclerView && mDrawDecorations != null && !mDrawDecorations.isEmpty()){ invalidate(); } } public interface DrawDecoration { void onDraw(@NonNull Canvas c, @NonNull QMUIStickySectionLayout parent); void onDrawOver(@NonNull Canvas c, @NonNull QMUIStickySectionLayout parent); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUIBasicTabSegment.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.widget.tab; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import com.qmuiteam.qmui.QMUIInterpolatorStaticHolder; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.layout.IQMUILayout; import com.qmuiteam.qmui.layout.QMUILayoutHelper; import com.qmuiteam.qmui.skin.IQMUISkinHandlerView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.IQMUISkinDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUIColorHelper; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** *

用于横向多个 Tab 的布局,可以灵活配置 Tab

*
    *
  • 可以用 xml 和 QMUITabSegment 提供的 set 方法统一配置文字颜色、icon 位置、是否要下划线等
  • *
  • 每个 Tab 都可以非常灵活的配置,如果没有提供相关配置,则使用 QMUIBasicTabSegment 提供的配置,具体参考 {@link QMUITab}
  • *
*

*

使用case:

*
    *
  • * 如果你希望自己设置 Tab 的文案或图片,那么通过{@link #addTab(QMUITab)}添加 Tab: * * QMUIBasicTabSegment mTabSegment = new QMUIBasicTabSegment((getContext()); * // config mTabSegment * QMUITabBuilder tabBuilder = mTabSegment.tabBuilder() * mTabSegment.addTab(tabBuilder.setText("item 1").build()); * mTabSegment.addTab(tabBuilder.setText("item 2").build()); * mTabSegment.notifyDataChanged(); * *
  • *
  • * 如果你想更改tab,则调用{@link #updateTabText(int, String)} 或者 {@link #replaceTab(int, QMUITab)} * * mTabSegment.updateTabText(1, "update item content"); * mTabSegment.replaceTab(1, tabBuilder.setText("replace item").build()); * *
  • *
  • * 如果你想更换全部Tab,需要在addTab前调用{@link #reset()}进行重置,addTab后调用{@link #notifyDataChanged()} 将数据应用到View上: * * mTabSegment.reset(); * // update mTabSegment with new config * QMUITabBuilder tabBuilder = mTabSegment.tabBuilder() * mTabSegment.addTab(tabBuilder.setText("new item 1").build()); * mTabSegment.addTab(tabBuilder.setText("new item 1").build()); * mTabSegment.notifyDataChanged(); * *
  • *
* * @author cginechen * @date 2016-01-27 */ public class QMUIBasicTabSegment extends HorizontalScrollView implements IQMUILayout, IQMUISkinHandlerView, IQMUISkinDefaultAttrProvider { private static final String TAG = "QMUIBasicTabSegment"; // mode: wrap content and scroll / match parent and avg item width public static final int MODE_SCROLLABLE = 0; public static final int MODE_FIXED = 1; public static final int NO_POSITION = -1; private final ArrayList mSelectedListeners = new ArrayList<>(); private Container mContentLayout; protected int mCurrentSelectedIndex = NO_POSITION; protected int mPendingSelectedIndex = NO_POSITION; private QMUITabIndicator mIndicator = null; private boolean mHideIndicatorWhenTabCountLessTwo = true; /** * TabSegmentMode */ @Mode private int mMode = MODE_FIXED; /** * item gap in MODE_SCROLLABLE */ private int mItemSpaceInScrollMode; private QMUITabAdapter mTabAdapter; protected QMUITabBuilder mTabBuilder; private boolean mSelectNoAnimation; protected Animator mSelectAnimator; private OnTabClickListener mOnTabClickListener; private boolean mIsInSelectTab = false; private QMUILayoutHelper mLayoutHelper; private static SimpleArrayMap sDefaultSkinAttrs; static { sDefaultSkinAttrs = new SimpleArrayMap<>(3); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BOTTOM_SEPARATOR, R.attr.qmui_skin_support_tab_separator_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.TOP_SEPARATOR, R.attr.qmui_skin_support_tab_separator_color); sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_tab_bg); } public QMUIBasicTabSegment(Context context) { this(context, null); } public QMUIBasicTabSegment(Context context, AttributeSet attrs) { this(context, attrs, R.attr.QMUITabSegmentStyle); } public QMUIBasicTabSegment(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setWillNotDraw(false); mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); init(context, attrs, defStyleAttr); setHorizontalScrollBarEnabled(false); setClipToPadding(false); setClipChildren(false); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUITabSegment, defStyleAttr, 0); // indicator boolean hasIndicator = array.getBoolean(R.styleable.QMUITabSegment_qmui_tab_has_indicator, false); int indicatorHeight = array.getDimensionPixelSize( R.styleable.QMUITabSegment_qmui_tab_indicator_height, getResources().getDimensionPixelSize(R.dimen.qmui_tab_segment_indicator_height)); boolean indicatorTop = array.getBoolean(R.styleable.QMUITabSegment_qmui_tab_indicator_top, false); boolean indicatorWidthFollowContent = array.getBoolean( R.styleable.QMUITabSegment_qmui_tab_indicator_with_follow_content, false); mIndicator = createTabIndicatorFromXmlInfo(hasIndicator, indicatorHeight, indicatorTop, indicatorWidthFollowContent); // tabBuilder int normalTextSize = array.getDimensionPixelSize( R.styleable.QMUITabSegment_android_textSize, getResources().getDimensionPixelSize(R.dimen.qmui_tab_segment_text_size)); normalTextSize = array.getDimensionPixelSize( R.styleable.QMUITabSegment_qmui_tab_normal_text_size, normalTextSize); int selectedTextSize = normalTextSize; selectedTextSize = array.getDimensionPixelSize( R.styleable.QMUITabSegment_qmui_tab_selected_text_size, selectedTextSize); mTabBuilder = new QMUITabBuilder(context) .setTextSize(normalTextSize, selectedTextSize) .setIconPosition(array.getInt(R.styleable.QMUITabSegment_qmui_tab_icon_position, QMUITab.ICON_POSITION_LEFT)); mMode = array.getInt(R.styleable.QMUITabSegment_qmui_tab_mode, MODE_FIXED); mItemSpaceInScrollMode = array.getDimensionPixelSize( R.styleable.QMUITabSegment_qmui_tab_space, QMUIDisplayHelper.dp2px(context, 10)); mSelectNoAnimation = array.getBoolean(R.styleable.QMUITabSegment_qmui_tab_select_no_animation, false); array.recycle(); mContentLayout = new Container(context); addView(mContentLayout, new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); mTabAdapter = createTabAdapter(mContentLayout); } public void setDefaultTextSize(int normalTextSize, int selectedTextSize) { mTabBuilder.setTextSize(normalTextSize, selectedTextSize); } public void setDefaultTabIconPosition(@QMUITab.IconPosition int iconPosition) { mTabBuilder.setIconPosition(iconPosition); } public void updateParentTabBuilder(TabBuilderUpdater updater) { updater.update(mTabBuilder); } protected QMUITabAdapter createTabAdapter(ViewGroup tabParentView) { return new QMUITabAdapter(this, tabParentView); } protected QMUITabIndicator createTabIndicatorFromXmlInfo(boolean hasIndicator, int indicatorHeight, boolean indicatorTop, boolean indicatorWidthFollowContent) { if (!hasIndicator) { return null; } return new QMUITabIndicator(indicatorHeight, indicatorTop, indicatorWidthFollowContent); } public QMUITabBuilder tabBuilder() { // do not change mTabBuilder to keep common config not changed return new QMUITabBuilder(mTabBuilder); } /** * replace with custom indicator * * @param indicator if null, present there is not a indicator */ public void setIndicator(@Nullable QMUITabIndicator indicator) { mIndicator = indicator; mContentLayout.requestLayout(); } public void setHideIndicatorWhenTabCountLessTwo(boolean hideIndicatorWhenTabCountLessTwo) { mHideIndicatorWhenTabCountLessTwo = hideIndicatorWhenTabCountLessTwo; } public void setItemSpaceInScrollMode(int itemSpaceInScrollMode) { mItemSpaceInScrollMode = itemSpaceInScrollMode; } /** * clear all tabs */ public void reset() { mTabAdapter.clear(); mCurrentSelectedIndex = NO_POSITION; if (mSelectAnimator != null) { mSelectAnimator.cancel(); mSelectAnimator = null; } } /** * clear select info */ public void resetSelect() { mCurrentSelectedIndex = NO_POSITION; mPendingSelectedIndex = NO_POSITION; if (mSelectAnimator != null) { mSelectAnimator.cancel(); mSelectAnimator = null; } } /** * add a tab to QMUITabSegment * * @param tab QMUITab * @return return this to chain */ public QMUIBasicTabSegment addTab(QMUITab tab) { mTabAdapter.addItem(tab); return this; } /** * notify dataChanged event to QMUITabSegment */ public void notifyDataChanged() { int current = mCurrentSelectedIndex; if(mPendingSelectedIndex != NO_POSITION){ current = mPendingSelectedIndex; } resetSelect(); mTabAdapter.setup(); selectTab(current); } public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { if (!mSelectedListeners.contains(listener)) { mSelectedListeners.add(listener); } } public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { mSelectedListeners.remove(listener); } public void clearOnTabSelectedListeners() { mSelectedListeners.clear(); } public int getMode() { return mMode; } public void setMode(@Mode int mode) { if (mMode != mode) { mMode = mode; if (mode == MODE_SCROLLABLE) { mTabBuilder.setGravity(Gravity.LEFT); } mContentLayout.invalidate(); } } protected void onClickTab(QMUITabView view, int index) { if (mSelectAnimator != null || needPreventEvent()) { return; } if (mOnTabClickListener != null) { if (mOnTabClickListener.onTabClick(view, index)) { return; } } QMUITab model = mTabAdapter.getItem(index); if (model != null) { selectTab(index, mSelectNoAnimation, true); } } protected boolean needPreventEvent() { return false; } void onDoubleClick(int index) { if (mSelectedListeners.isEmpty()) { return; } QMUITab model = mTabAdapter.getItem(index); if (model != null) { dispatchTabDoubleTap(index); } } private void dispatchTabSelected(int index) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabSelected(index); } } private void dispatchTabUnselected(int index) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabUnselected(index); } } private void dispatchTabReselected(int index) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabReselected(index); } } private void dispatchTabDoubleTap(int index) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onDoubleTap(index); } } public void setSelectNoAnimation(boolean noAnimation) { mSelectNoAnimation = noAnimation; } public void selectTab(int index) { selectTab(index, mSelectNoAnimation, false); } public void selectTab(final int index, boolean noAnimation, boolean fromTabClick) { if (mIsInSelectTab) { return; } mIsInSelectTab = true; List listViews = mTabAdapter.getViews(); if (listViews.size() != mTabAdapter.getSize()) { mTabAdapter.setup(); listViews = mTabAdapter.getViews(); } if (listViews.size() == 0 || listViews.size() <= index) { mIsInSelectTab = false; return; } if (mSelectAnimator != null || needPreventEvent()) { mPendingSelectedIndex = index; mIsInSelectTab = false; return; } if (mCurrentSelectedIndex == index) { if (fromTabClick) { // dispatch re select only when click tab dispatchTabReselected(index); } mIsInSelectTab = false; // invalidate mContentLayout to sure indicator is drawn if needed mContentLayout.invalidate(); return; } if (mCurrentSelectedIndex > listViews.size()) { Log.i(TAG, "selectTab: current selected index is bigger than views size."); mCurrentSelectedIndex = NO_POSITION; } // first time to select if (mCurrentSelectedIndex == NO_POSITION) { QMUITab model = mTabAdapter.getItem(index); layoutIndicator(model, true); QMUITabView tabView = listViews.get(index); tabView.setSelected(true); // 标记选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 tabView.setSelectFraction(1f); dispatchTabSelected(index); mCurrentSelectedIndex = index; mIsInSelectTab = false; return; } final int prev = mCurrentSelectedIndex; final QMUITab prevModel = mTabAdapter.getItem(prev); final QMUITabView prevView = listViews.get(prev); final QMUITab nowModel = mTabAdapter.getItem(index); final QMUITabView nowView = listViews.get(index); if (noAnimation) { dispatchTabUnselected(prev); dispatchTabSelected(index); prevView.setSelectFraction(0f); prevView.setSelected(false); // 标记未选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 nowView.setSelectFraction(1f); nowView.setSelected(true); // 标记选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 if (mMode == MODE_SCROLLABLE) { int scrollX = getScrollX(), w = getWidth(), cw = mContentLayout.getWidth(), nl = nowView.getLeft(), nw = nowView.getWidth(); int paddingHor = getPaddingLeft() + getPaddingRight(); int size = mTabAdapter.getSize(); int maxScrollX = cw - w + paddingHor; if (index > prev) { if (index >= size - 2) { smoothScrollBy(maxScrollX - scrollX, 0); } else { int nextWidth = listViews.get(index + 1).getWidth(); int targetScrollX = Math.min(maxScrollX, nl - (w - getPaddingRight() * 2 - nextWidth - nw - mItemSpaceInScrollMode)); targetScrollX -= nextWidth - nw; if (scrollX < targetScrollX) { smoothScrollBy(targetScrollX - scrollX, 0); } } } else { if (index <= 1) { smoothScrollBy(-scrollX, 0); } else { int prevWidth = listViews.get(index - 1).getWidth(); int targetScrollX = Math.max(0, nl - prevWidth - mItemSpaceInScrollMode); if (targetScrollX < scrollX) { smoothScrollBy(targetScrollX - scrollX, 0); } } } } mCurrentSelectedIndex = index; mIsInSelectTab = false; layoutIndicator(nowModel, true); return; } final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setInterpolator(QMUIInterpolatorStaticHolder.LINEAR_INTERPOLATOR); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float animValue = (float) animation.getAnimatedValue(); prevView.setSelectFraction(1 - animValue); nowView.setSelectFraction(animValue); layoutIndicatorInTransition(prevModel, nowModel, animValue); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { mSelectAnimator = animation; } @Override public void onAnimationEnd(Animator animation) { prevView.setSelectFraction(0f); prevView.setSelected(false); // 标记未选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 nowView.setSelectFraction(1f); nowView.setSelected(true); // 标记选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 mSelectAnimator = null; // set current selected index first, dispatchTabSelected may call selectTab again. mCurrentSelectedIndex = index; dispatchTabSelected(index); dispatchTabUnselected(prev); if (mPendingSelectedIndex != NO_POSITION && !needPreventEvent()) { selectTab(mPendingSelectedIndex, true, false); mPendingSelectedIndex = NO_POSITION; } } @Override public void onAnimationCancel(Animator animation) { mSelectAnimator = null; prevView.setSelectFraction(1f); prevView.setSelected(true); // 标记选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 nowView.setSelectFraction(0f); nowView.setSelected(false); // 标记未选中,使得TalkBack等屏幕阅读器可向用户报告tab状态 layoutIndicator(prevModel, true); } @Override public void onAnimationRepeat(Animator animation) { } }); animator.setDuration(200); animator.start(); mIsInSelectTab = false; } private void layoutIndicator(QMUITab model, boolean invalidate) { if (model == null || mIndicator == null) { return; } mIndicator.updateInfo(model.contentLeft, model.contentWidth, model.selectedColorAttr == 0 ? model.selectColor : QMUISkinHelper.getSkinColor(this, model.selectedColorAttr), 0f); if (invalidate) { mContentLayout.invalidate(); } } private void layoutIndicatorInTransition(QMUITab preModel, QMUITab targetModel, float offsetPercent) { if (mIndicator == null) { return; } final int leftDistance = targetModel.contentLeft - preModel.contentLeft; final int widthDistance = targetModel.contentWidth - preModel.contentWidth; final int targetLeft = (int) (preModel.contentLeft + leftDistance * offsetPercent); final int targetWidth = (int) (preModel.contentWidth + widthDistance * offsetPercent); int indicatorColor = QMUIColorHelper.computeColor( preModel.selectedColorAttr == 0 ? preModel.selectColor : QMUISkinHelper.getSkinColor(this, preModel.selectedColorAttr), targetModel.selectedColorAttr == 0 ? targetModel.selectColor : QMUISkinHelper.getSkinColor(this, targetModel.selectedColorAttr), offsetPercent); mIndicator.updateInfo(targetLeft, targetWidth, indicatorColor, offsetPercent); mContentLayout.invalidate(); } public void updateIndicatorPosition(final int index, float offsetPercent) { if (mSelectAnimator != null || mIsInSelectTab || offsetPercent == 0) { return; } int targetIndex; if (offsetPercent < 0) { targetIndex = index - 1; offsetPercent = -offsetPercent; } else { targetIndex = index + 1; } final List listViews = mTabAdapter.getViews(); if (listViews.size() <= index || listViews.size() <= targetIndex) { return; } QMUITab preModel = mTabAdapter.getItem(index); QMUITab targetModel = mTabAdapter.getItem(targetIndex); QMUITabView preView = listViews.get(index); QMUITabView targetView = listViews.get(targetIndex); preView.setSelectFraction(1 - offsetPercent); targetView.setSelectFraction(offsetPercent); layoutIndicatorInTransition(preModel, targetModel, offsetPercent); } /** * 改变 Tab 的文案 * * @param index Tab 的 index * @param text 新文案 */ public void updateTabText(int index, String text) { QMUITab model = mTabAdapter.getItem(index); if (model == null) { return; } model.setText(text); notifyDataChanged(); } /** * 整个 Tab 替换 * * @param index 需要被替换的 Tab 的 index * @param model 新的 Tab */ public void replaceTab(int index, QMUITab model) { try { if (mCurrentSelectedIndex == index) { // re select mCurrentSelectedIndex = NO_POSITION; } mTabAdapter.replaceItem(index, model); notifyDataChanged(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public void setOnTabClickListener(OnTabClickListener onTabClickListener) { mOnTabClickListener = onTabClickListener; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (getChildCount() > 0) { final View child = getChildAt(0); int paddingHor = getPaddingLeft() + getPaddingRight(); child.measure(MeasureSpec.makeMeasureSpec(widthSize - paddingHor, MeasureSpec.EXACTLY), heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(Math.min(widthSize, child.getMeasuredWidth() + paddingHor), heightMeasureSpec); return; } } setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } public int getSelectedIndex() { return mCurrentSelectedIndex; } public int getTabCount() { return mTabAdapter.getSize(); } /** * get {@link QMUITab} by index * * @param index index * @return QMUITab */ public QMUITab getTab(int index) { return mTabAdapter.getItem(index); } /** * show signCount/redPoint by index * * @param index the index of tab * @param count if count > 0, show signCount; else if count == 0 show redPoint; else show nothing */ public void showSignCountView(Context context, int index, int count) { QMUITab tab = mTabAdapter.getItem(index); tab.setSignCount(count); notifyDataChanged(); } /** * clear signCount/redPoint by index * * @param index the index of tab */ public void clearSignCountView(int index) { QMUITab tab = mTabAdapter.getItem(index); tab.clearSignCountOrRedPoint(); notifyDataChanged(); } /** * get sign count by index * * @param index the index of tab */ public int getSignCount(int index) { QMUITab tab = mTabAdapter.getItem(index); return tab.getSignCount(); } /** * is redPoint showing ? * * @param index the index of tab * @return true if redPoint is showing */ public boolean isRedPointShowing(int index) { return mTabAdapter.getItem(index).isRedPointShowing(); } @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public interface OnTabClickListener { /** * 当某个 Tab 被点击时会触发 * * @param tabView 被点击的View * @param index 被点击的 Tab 下标 * * @return true 拦截 selectTab 事件 */ boolean onTabClick(QMUITabView tabView, int index); } public interface OnTabSelectedListener { /** * 当某个 Tab 被选中时会触发 * * @param index 被选中的 Tab 下标 */ void onTabSelected(int index); /** * 当某个 Tab 被取消选中时会触发 * * @param index 被取消选中的 Tab 下标 */ void onTabUnselected(int index); /** * 当某个 Tab 处于被选中状态下再次被点击时会触发 * * @param index 被再次点击的 Tab 下标 */ void onTabReselected(int index); /** * 当某个 Tab 被双击时会触发 * * @param index 被双击的 Tab 下标 */ void onDoubleTap(int index); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mCurrentSelectedIndex != NO_POSITION && mMode == MODE_SCROLLABLE) { final QMUITabView view = mTabAdapter.getViews().get(mCurrentSelectedIndex); if (getScrollX() > view.getLeft()) { scrollTo(view.getLeft(), 0); } else { int realWidth = getWidth() - getPaddingRight() - getPaddingLeft(); if (getScrollX() + realWidth < view.getRight()) { scrollBy(view.getRight() - realWidth - getScrollX(), 0); } } } } private final class Container extends ViewGroup { public Container(Context context) { super(context); setClipChildren(false); setWillNotDraw(false); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); List childViews = mTabAdapter.getViews(); int size = childViews.size(); int i; int visibleChild = 0; for (i = 0; i < size; i++) { View child = childViews.get(i); if (child.getVisibility() == VISIBLE) { visibleChild++; } } if (size == 0 || visibleChild == 0) { setMeasuredDimension(widthSpecSize, heightSpecSize); return; } int childHeight = heightSpecSize - getPaddingTop() - getPaddingBottom(); int childWidthMeasureSpec, childHeightMeasureSpec, resultWidthSize = 0; if (mMode == MODE_FIXED) { resultWidthSize = widthSpecSize; int modeFixItemWidth = widthSpecSize / visibleChild; for (i = 0; i < size; i++) { final View child = childViews.get(i); if (child.getVisibility() != VISIBLE) { continue; } childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(modeFixItemWidth, MeasureSpec.EXACTLY); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // reset QMUITab tab = mTabAdapter.getItem(i); tab.leftAddonMargin = 0; tab.rightAddonMargin = 0; } } else { float totalWeight = 0; for (i = 0; i < size; i++) { final View child = childViews.get(i); if (child.getVisibility() != VISIBLE) { continue; } childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.AT_MOST); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); resultWidthSize += child.getMeasuredWidth() + mItemSpaceInScrollMode; QMUITab tab = mTabAdapter.getItem(i); totalWeight += tab.leftSpaceWeight + tab.rightSpaceWeight; // reset first tab.leftAddonMargin = 0; tab.rightAddonMargin = 0; } resultWidthSize -= mItemSpaceInScrollMode; if (totalWeight > 0 && resultWidthSize < widthSpecSize) { int remain = widthSpecSize - resultWidthSize; resultWidthSize = widthSpecSize; for (i = 0; i < size; i++) { final View child = childViews.get(i); if (child.getVisibility() != VISIBLE) { continue; } QMUITab tab = mTabAdapter.getItem(i); tab.leftAddonMargin = (int) (remain * tab.leftSpaceWeight / totalWeight); tab.rightAddonMargin = (int) (remain * tab.rightSpaceWeight / totalWeight); } } } setMeasuredDimension(resultWidthSize, heightSpecSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { List childViews = mTabAdapter.getViews(); int size = childViews.size(); int i; int visibleChild = 0; for (i = 0; i < size; i++) { View child = childViews.get(i); if (child.getVisibility() == VISIBLE) { visibleChild++; } } if (size == 0 || visibleChild == 0) { return; } int usedLeft = getPaddingLeft(); for (i = 0; i < size; i++) { QMUITabView childView = childViews.get(i); if (childView.getVisibility() != VISIBLE) { continue; } final int childMeasureWidth = childView.getMeasuredWidth(); QMUITab model = mTabAdapter.getItem(i); usedLeft += model.leftAddonMargin; childView.layout(usedLeft, getPaddingTop(), usedLeft + childMeasureWidth, b - t - getPaddingBottom()); int oldLeft, oldWidth, newLeft, newWidth; oldLeft = model.contentLeft; oldWidth = model.contentWidth; if (mMode == MODE_FIXED && (mIndicator != null && mIndicator.isIndicatorWidthFollowContent())) { newLeft = usedLeft + childView.getContentViewLeft(); newWidth = childView.getContentViewWidth(); } else { newLeft = usedLeft; newWidth = childMeasureWidth; } if (oldLeft != newLeft || oldWidth != newWidth) { model.contentLeft = newLeft; model.contentWidth = newWidth; } usedLeft = usedLeft + childMeasureWidth + model.rightAddonMargin + (mMode == MODE_SCROLLABLE ? mItemSpaceInScrollMode : 0); } if (mCurrentSelectedIndex != NO_POSITION && mSelectAnimator == null && !needPreventEvent()) { layoutIndicator(mTabAdapter.getItem(mCurrentSelectedIndex), false); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mIndicator != null && (!mHideIndicatorWhenTabCountLessTwo || mTabAdapter.getSize() > 1)) { mIndicator.draw(this, canvas, getPaddingTop(), getHeight() - getPaddingBottom()); } } } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override protected void onDraw(Canvas canvas) { mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); super.onDraw(canvas); } @Override public SimpleArrayMap getDefaultSkinAttrs() { return sDefaultSkinAttrs; } @Override public void handle(@NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme, @Nullable SimpleArrayMap attrs) { manager.defaultHandleSkinAttrs(this, theme, attrs); if (mIndicator != null) { mIndicator.handleSkinChange(manager, skinIndex, theme, mTabAdapter.getItem(mCurrentSelectedIndex)); mContentLayout.invalidate(); } } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } public interface TabBuilderUpdater { void update(QMUITabBuilder builder); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITab.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.widget.tab; import android.graphics.Typeface; import android.view.Gravity; import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import com.qmuiteam.qmui.skin.QMUISkinHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class QMUITab { public static final int ICON_POSITION_LEFT = 0; public static final int ICON_POSITION_TOP = 1; public static final int ICON_POSITION_RIGHT = 2; public static final int ICON_POSITION_BOTTOM = 3; public static final int SIGN_COUNT_VERTICAL_ALIGN_BOTTOM_TO_CONTENT_TOP = 0; public static final int SIGN_COUNT_VERTICAL_ALIGN_TOP_TO_CONTENT_TOP = 1; public static final int SIGN_COUNT_VERTICAL_ALIGN_MIDDLE_TO_CONTENT = 2; public static final int NO_SIGN_COUNT_AND_RED_POINT = 0; public static final int RED_POINT_SIGN_COUNT = -1; @IntDef(value = { ICON_POSITION_LEFT, ICON_POSITION_TOP, ICON_POSITION_RIGHT, ICON_POSITION_BOTTOM}) @Retention(RetentionPolicy.SOURCE) public @interface IconPosition { } boolean allowIconDrawOutside; int iconTextGap; int normalTextSize; int selectedTextSize; Typeface normalTypeface; Typeface selectedTypeface; float typefaceUpdateAreaPercent; int normalColor; int selectColor; int normalColorAttr; int selectedColorAttr; int normalTabIconWidth = QMUITabIcon.TAB_ICON_INTRINSIC; int normalTabIconHeight = QMUITabIcon.TAB_ICON_INTRINSIC; float selectedTabIconScale = 1f; QMUITabIcon tabIcon = null; boolean skinChangeWithTintColor; boolean skinChangeNormalWithTintColor; boolean skinChangeSelectedWithTintColor; int normalIconAttr; int selectedIconAttr; int contentWidth = 0; int contentLeft = 0; @IconPosition int iconPosition = ICON_POSITION_TOP; int gravity = Gravity.CENTER; private CharSequence text; private CharSequence description; int signCountDigits = 2; int signCountHorizontalOffset = 0; int signCountVerticalOffset = 0; int signCountVerticalAlign = SIGN_COUNT_VERTICAL_ALIGN_BOTTOM_TO_CONTENT_TOP; int signCount = NO_SIGN_COUNT_AND_RED_POINT; float rightSpaceWeight = 0f; float leftSpaceWeight = 0f; int leftAddonMargin = 0; int rightAddonMargin = 0; QMUITab(CharSequence text) { this(text, text); } QMUITab(CharSequence text, CharSequence description) { this.text = text; this.description = description; } public CharSequence getText() { return text; } public void setText(CharSequence text) { this.text = text; } public void setDescription(CharSequence description) { this.description = description; } public CharSequence getDescription() { return description; } public int getIconPosition() { return iconPosition; } public void setIconPosition(@IconPosition int iconPosition) { this.iconPosition = iconPosition; } public void setSpaceWeight(float leftWeight, float rightWeight) { leftSpaceWeight = leftWeight; rightSpaceWeight = rightWeight; } public void setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { this.typefaceUpdateAreaPercent = typefaceUpdateAreaPercent; } public float getTypefaceUpdateAreaPercent() { return typefaceUpdateAreaPercent; } public int getGravity() { return gravity; } public void setGravity(int gravity) { this.gravity = gravity; } public void setSignCount(int signCount) { this.signCount = signCount; } public void setRedPoint(){ this.signCount = RED_POINT_SIGN_COUNT; } public int getSignCount(){ return this.signCount; } public boolean isRedPointShowing(){ return this.signCount == RED_POINT_SIGN_COUNT; } public void clearSignCountOrRedPoint(){ this.signCount = NO_SIGN_COUNT_AND_RED_POINT; } public int getNormalColor(@NonNull View skinFollowView) { if(normalColorAttr == 0){ return normalColor; } return QMUISkinHelper.getSkinColor(skinFollowView, normalColorAttr); } public int getSelectColor(@NonNull View skinFollowView) { if(selectedColorAttr == 0){ return selectColor; } return QMUISkinHelper.getSkinColor(skinFollowView, selectedColorAttr); } public int getNormalColorAttr() { return normalColorAttr; } public int getSelectedColorAttr() { return selectedColorAttr; } public int getNormalIconAttr() { return normalIconAttr; } public int getSelectedIconAttr() { return selectedIconAttr; } public int getNormalTextSize() { return normalTextSize; } public int getSelectedTextSize() { return selectedTextSize; } public QMUITabIcon getTabIcon() { return tabIcon; } public Typeface getNormalTypeface() { return normalTypeface; } public Typeface getSelectedTypeface() { return selectedTypeface; } public int getNormalTabIconWidth() { if (normalTabIconWidth == QMUITabIcon.TAB_ICON_INTRINSIC && tabIcon != null) { return tabIcon.getIntrinsicWidth(); } return normalTabIconWidth; } public int getNormalTabIconHeight() { if (normalTabIconHeight == QMUITabIcon.TAB_ICON_INTRINSIC && tabIcon != null) { return tabIcon.getIntrinsicWidth(); } return normalTabIconHeight; } public float getSelectedTabIconScale() { return selectedTabIconScale; } public int getIconTextGap() { return iconTextGap; } public boolean isAllowIconDrawOutside() { return allowIconDrawOutside; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabAdapter.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.widget.tab; import android.view.ViewGroup; import com.qmuiteam.qmui.widget.QMUIItemViewsAdapter; public class QMUITabAdapter extends QMUIItemViewsAdapter implements QMUITabView.Callback { private QMUIBasicTabSegment mTabSegment; public QMUITabAdapter(QMUIBasicTabSegment tabSegment, ViewGroup parentView) { super(parentView); mTabSegment = tabSegment; } @Override protected QMUITabView createView(ViewGroup parentView) { return new QMUITabView(parentView.getContext()); } @Override protected final void bind(QMUITab item, QMUITabView view, int position) { onBindTab(item, view, position); view.setCallback(this); // reset if (view.getSelectFraction() != 0f || view.isSelected()) { view.setSelected(false); view.setSelectFraction(0f); } } @Override protected void onViewRecycled(QMUITabView qmuiTabView) { qmuiTabView.setSelected(false); qmuiTabView.setSelectFraction(0f); qmuiTabView.setCallback(null); } protected void onBindTab(QMUITab item, QMUITabView view, int position) { view.bind(item); } @Override public void onClick(QMUITabView view) { int index = getViews().indexOf(view); mTabSegment.onClickTab(view, index); } @Override public void onDoubleClick(QMUITabView view) { int index = getViews().indexOf(view); mTabSegment.onDoubleClick(index); } @Override public void onLongClick(QMUITabView view) { } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabBuilder.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.widget.tab; import android.content.Context; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.view.Gravity; import androidx.annotation.Nullable; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; /** * use {@link QMUITabSegment#tabBuilder()} to get a instance */ public class QMUITabBuilder { /** * icon in normal state */ private int normalDrawableAttr = 0; private @Nullable Drawable normalDrawable; /** * icon in selected state */ private int selectedDrawableAttr = 0; private @Nullable Drawable selectedDrawable; /** * change icon by tint color, if true, selectedDrawable will not work */ private boolean dynamicChangeIconColor = false; /** * for skin change. if true, then normalDrawableAttr and selectedDrawableAttr will not work. * otherwise, icon will be replaced by normalDrawableAttr and selectedDrawableAttr */ private boolean skinChangeWithTintColor = false; private boolean skinChangeNormalWithTintColor = true; private boolean skinChangeSelectedWithTintColor = true; /** * text size in normal state */ private int normalTextSize; /** * text size in selected state */ private int selectTextSize; /** * text color(icon color in if dynamicChangeIconColor == true) in normal state */ private int normalColorAttr = R.attr.qmui_skin_support_tab_normal_color; /** * text color(icon color in if dynamicChangeIconColor == true) in selected state */ private int selectedColorAttr = R.attr.qmui_skin_support_tab_selected_color; /** * text color with no skin support */ private int normalColor = 0; /** * text color with no skin support */ private int selectColor = 0; /** * icon position(left/top/right/bottom) */ private @QMUITab.IconPosition int iconPosition = QMUITab.ICON_POSITION_TOP; /** * gravity of text */ private int gravity = Gravity.CENTER; private CharSequence text; private CharSequence description; /** * text typeface in normal state */ private Typeface normalTypeface; /** * text typeface in selected state */ private Typeface selectedTypeface; /** * width of tab icon in normal state */ private int normalTabIconWidth = QMUITabIcon.TAB_ICON_INTRINSIC; /** * height of tab icon in normal state */ int normalTabIconHeight = QMUITabIcon.TAB_ICON_INTRINSIC; /** * scale of tab icon in selected state */ float selectedTabIconScale = 1f; float typefaceUpdateAreaPercent = 0.25f; /** * signCount or redPoint */ private int signCount = QMUITab.NO_SIGN_COUNT_AND_RED_POINT; /** * max signCount digits, if the number is over the digits, use 'xx+' to present * if signCountDigits == 2 and number is 110, then component will show '99+' */ private int signCountDigits = 2; /** * the horizontal offset of signCount(redPoint) view */ private int signCountHorizontalOffset; /** * the vertical offset of signCount(redPoint) view */ private int signCountVerticalOffset; private int signCountVerticalAlign = QMUITab.SIGN_COUNT_VERTICAL_ALIGN_BOTTOM_TO_CONTENT_TOP; /** * the gap between icon and text */ private int iconTextGap; /** * allow icon draw outside of tab view */ private boolean allowIconDrawOutside = true; QMUITabBuilder(Context context) { iconTextGap = QMUIDisplayHelper.dp2px(context, 2); normalTextSize = selectTextSize = QMUIDisplayHelper.dp2px(context, 12); signCountHorizontalOffset = QMUIDisplayHelper.dp2px(context, 3); signCountVerticalOffset = signCountHorizontalOffset; } QMUITabBuilder(QMUITabBuilder other) { this.normalDrawableAttr = other.normalDrawableAttr; this.selectedDrawableAttr = other.selectedDrawableAttr; this.normalDrawable = other.normalDrawable; this.selectedDrawable = other.selectedDrawable; this.dynamicChangeIconColor = other.dynamicChangeIconColor; this.normalTextSize = other.normalTextSize; this.selectTextSize = other.selectTextSize; this.normalColorAttr = other.normalColorAttr; this.selectedColorAttr = other.selectedColorAttr; this.iconPosition = other.iconPosition; this.gravity = other.gravity; this.text = other.text; this.description = other.description; this.signCount = other.signCount; this.signCountDigits = other.signCountDigits; this.signCountHorizontalOffset = other.signCountHorizontalOffset; this.signCountVerticalOffset = other.signCountVerticalOffset; this.signCountVerticalAlign = other.signCountVerticalAlign; this.normalTypeface = other.normalTypeface; this.selectedTypeface = other.selectedTypeface; this.normalTabIconWidth = other.normalTabIconWidth; this.normalTabIconHeight = other.normalTabIconHeight; this.selectedTabIconScale = other.selectedTabIconScale; this.iconTextGap = other.iconTextGap; this.allowIconDrawOutside = other.allowIconDrawOutside; this.typefaceUpdateAreaPercent = other.typefaceUpdateAreaPercent; this.skinChangeNormalWithTintColor = other.skinChangeNormalWithTintColor; this.skinChangeSelectedWithTintColor = other.skinChangeSelectedWithTintColor; this.skinChangeWithTintColor = other.skinChangeWithTintColor; this.normalColor = other.normalColor; this.selectColor = other.selectColor; } public QMUITabBuilder setAllowIconDrawOutside(boolean allowIconDrawOutside) { this.allowIconDrawOutside = allowIconDrawOutside; return this; } public QMUITabBuilder setTypefaceUpdateAreaPercent(float typefaceUpdateAreaPercent) { this.typefaceUpdateAreaPercent = typefaceUpdateAreaPercent; return this; } public QMUITabBuilder setNormalDrawable(Drawable normalDrawable) { this.normalDrawable = normalDrawable; return this; } public QMUITabBuilder setNormalDrawableAttr(int normalDrawableAttr) { this.normalDrawableAttr = normalDrawableAttr; return this; } public QMUITabBuilder setSelectedDrawable(Drawable selectedDrawable) { this.selectedDrawable = selectedDrawable; return this; } public QMUITabBuilder setSelectedDrawableAttr(int selectedDrawableAttr) { this.selectedDrawableAttr = selectedDrawableAttr; return this; } @Deprecated public QMUITabBuilder skinChangeWithTintColor(boolean skinChangeWithTintColor){ this.skinChangeWithTintColor = skinChangeWithTintColor; return this; } public QMUITabBuilder skinChangeNormalWithTintColor(boolean skinChangeNormalWithTintColor){ this.skinChangeNormalWithTintColor = skinChangeNormalWithTintColor; return this; } public QMUITabBuilder skinChangeSelectedWithTintColor(boolean skinChangeSelectedWithTintColor){ this.skinChangeSelectedWithTintColor = skinChangeSelectedWithTintColor; return this; } public QMUITabBuilder setTextSize(int normalTextSize, int selectedTextSize) { this.normalTextSize = normalTextSize; this.selectTextSize = selectedTextSize; return this; } public QMUITabBuilder setTypeface(Typeface normalTypeface, Typeface selectedTypeface) { this.normalTypeface = normalTypeface; this.selectedTypeface = selectedTypeface; return this; } public QMUITabBuilder setNormalIconSizeInfo(int normalWidth, int normalHeight) { this.normalTabIconWidth = normalWidth; this.normalTabIconHeight = normalHeight; return this; } public QMUITabBuilder setSelectedIconScale(float selectedScale) { this.selectedTabIconScale = selectedScale; return this; } public QMUITabBuilder setIconTextGap(int iconTextGap) { this.iconTextGap = iconTextGap; return this; } public QMUITabBuilder setSignCount(int signCount) { this.signCount = signCount; return this; } public QMUITabBuilder setSignCountMarginInfo(int digit, int horizontalOffset, int verticalOffset){ return setSignCountMarginInfo(digit, horizontalOffset, QMUITab.SIGN_COUNT_VERTICAL_ALIGN_BOTTOM_TO_CONTENT_TOP, verticalOffset); } public QMUITabBuilder setSignCountMarginInfo(int digit, int horizontalOffset, int verticalAlign, int verticalOffset ) { this.signCountDigits = digit; this.signCountHorizontalOffset = horizontalOffset; this.signCountVerticalOffset = verticalOffset; this.signCountVerticalAlign = verticalAlign; return this; } public QMUITabBuilder setColorAttr(int normalColorAttr, int selectedColorAttr) { this.normalColorAttr = normalColorAttr; this.selectedColorAttr = selectedColorAttr; return this; } public QMUITabBuilder setNormalColorAttr(int normalColorAttr) { this.normalColorAttr = normalColorAttr; return this; } public QMUITabBuilder setSelectedColorAttr(int selectedColorAttr) { this.selectedColorAttr = selectedColorAttr; return this; } public QMUITabBuilder setColor(int normalColor, int selectColor){ this.normalColorAttr = 0; this.selectedColorAttr = 0; this.normalColor = normalColor; this.selectColor = selectColor; return this; } public QMUITabBuilder setNormalColor(int normalColor) { this.normalColorAttr = 0; this.normalColor = normalColor; return this; } public QMUITabBuilder setSelectColor(int selectColor) { this.selectedColorAttr = 0; this.selectColor = selectColor; return this; } public QMUITabBuilder setDynamicChangeIconColor(boolean dynamicChangeIconColor) { this.dynamicChangeIconColor = dynamicChangeIconColor; return this; } public QMUITabBuilder setGravity(int gravity) { this.gravity = gravity; return this; } public QMUITabBuilder setIconPosition(@QMUITab.IconPosition int iconPosition) { this.iconPosition = iconPosition; return this; } public QMUITabBuilder setText(CharSequence text) { this.text = text; return this; } public QMUITabBuilder setDescription(CharSequence description){ this.description = description; return this; } public QMUITab build(Context context) { QMUITab tab = new QMUITab(text, description == null ? text : description); if(!skinChangeWithTintColor){ if(!skinChangeNormalWithTintColor){ if(normalDrawableAttr != 0){ normalDrawable = QMUIResHelper.getAttrDrawable(context, normalDrawableAttr); } } if(!skinChangeSelectedWithTintColor){ if(selectedDrawableAttr != 0){ selectedDrawable = QMUIResHelper.getAttrDrawable(context, selectedDrawableAttr); } } } tab.skinChangeWithTintColor = this.skinChangeWithTintColor; tab.skinChangeNormalWithTintColor = this.skinChangeNormalWithTintColor; tab.skinChangeSelectedWithTintColor = this.skinChangeSelectedWithTintColor; if (normalDrawable != null) { if (dynamicChangeIconColor || selectedDrawable == null) { tab.tabIcon = new QMUITabIcon(normalDrawable, null, true); // must same tab.skinChangeSelectedWithTintColor = tab.skinChangeNormalWithTintColor; } else { tab.tabIcon = new QMUITabIcon(normalDrawable, selectedDrawable, false); } tab.tabIcon.setBounds(0, 0, normalTabIconWidth, normalTabIconHeight); } tab.normalIconAttr = this.normalDrawableAttr; tab.selectedIconAttr = this.selectedDrawableAttr; tab.normalTabIconWidth = this.normalTabIconWidth; tab.normalTabIconHeight = this.normalTabIconHeight; tab.selectedTabIconScale = this.selectedTabIconScale; tab.gravity = this.gravity; tab.iconPosition = this.iconPosition; tab.normalTextSize = this.normalTextSize; tab.selectedTextSize = this.selectTextSize; tab.normalTypeface = this.normalTypeface; tab.selectedTypeface = this.selectedTypeface; tab.normalColorAttr = this.normalColorAttr; tab.selectedColorAttr = this.selectedColorAttr; tab.normalColor = this.normalColor; tab.selectColor = this.selectColor; tab.signCount = this.signCount; tab.signCountDigits = this.signCountDigits; tab.signCountHorizontalOffset = this.signCountHorizontalOffset; tab.signCountVerticalAlign = this.signCountVerticalAlign; tab.signCountVerticalOffset = this.signCountVerticalOffset; tab.iconTextGap = this.iconTextGap; tab.typefaceUpdateAreaPercent = this.typefaceUpdateAreaPercent; return tab; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIcon.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.widget.tab; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; import com.qmuiteam.qmui.util.QMUIColorHelper; import com.qmuiteam.qmui.util.QMUILangHelper; public class QMUITabIcon extends Drawable implements Drawable.Callback { public static final int TAB_ICON_INTRINSIC = -1; private @NonNull Drawable mNormalIconDrawable; private @Nullable Drawable mSelectedIconDrawable; private float mCurrentSelectFraction = 0f; private boolean mDynamicChangeIconColor = true; public QMUITabIcon(@NonNull Drawable normalIconDrawable, @Nullable Drawable selectedIconDrawable){ this(normalIconDrawable, selectedIconDrawable, true); } public QMUITabIcon(@NonNull Drawable normalIconDrawable, @Nullable Drawable selectedIconDrawable, boolean dynamicChangeIconColor) { mNormalIconDrawable = normalIconDrawable.mutate(); mNormalIconDrawable.setCallback(this); if (selectedIconDrawable != null) { mSelectedIconDrawable = selectedIconDrawable.mutate(); mSelectedIconDrawable.setCallback(this); } mNormalIconDrawable.setAlpha(255); int nw = mNormalIconDrawable.getIntrinsicWidth(); int nh = mNormalIconDrawable.getIntrinsicHeight(); mNormalIconDrawable.setBounds(0, 0, nw, nh); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.setAlpha(0); mSelectedIconDrawable.setBounds(0, 0, nw, nh); } mDynamicChangeIconColor = dynamicChangeIconColor; } public boolean hasSelectedIcon() { return mSelectedIconDrawable != null; } public void tint(int normalColor, int selectColor) { if (mSelectedIconDrawable == null) { DrawableCompat.setTint(mNormalIconDrawable, QMUIColorHelper.computeColor(normalColor, selectColor, mCurrentSelectFraction)); } else { DrawableCompat.setTint(mNormalIconDrawable, normalColor); DrawableCompat.setTint(mSelectedIconDrawable, selectColor); } invalidateSelf(); } public void tintNormal(int normalColor){ DrawableCompat.setTint(mNormalIconDrawable, normalColor); invalidateSelf(); } public void tintSelected(int selectColor){ if (mSelectedIconDrawable != null) { DrawableCompat.setTint(mSelectedIconDrawable, selectColor); invalidateSelf(); } } public void srcNormal(@NonNull Drawable normalDrawable){ int normalAlpha = (int) (255 * (1 - mCurrentSelectFraction)); mNormalIconDrawable.setCallback(null); mNormalIconDrawable = normalDrawable.mutate(); mNormalIconDrawable.setCallback(this); mNormalIconDrawable.setAlpha(normalAlpha); invalidateSelf(); } public void srcSelected(@NonNull Drawable selectDrawable){ int selectedAlpha = (int) (255 * mCurrentSelectFraction); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.setCallback(null); } mSelectedIconDrawable = selectDrawable.mutate(); mSelectedIconDrawable.setCallback(this); mSelectedIconDrawable.setAlpha(selectedAlpha); invalidateSelf(); } public void src(@NonNull Drawable normalDrawable, @NonNull Drawable selectDrawable) { int normalAlpha = (int) (255 * (1 - mCurrentSelectFraction)); mNormalIconDrawable.setCallback(null); mNormalIconDrawable = normalDrawable.mutate(); mNormalIconDrawable.setCallback(this); mNormalIconDrawable.setAlpha(normalAlpha); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.setCallback(null); } mSelectedIconDrawable = selectDrawable.mutate(); mSelectedIconDrawable.setCallback(this); mSelectedIconDrawable.setAlpha(255 - normalAlpha); invalidateSelf(); } public void src(@NonNull Drawable normalDrawable, int normalColor, int selectColor) { mNormalIconDrawable.setCallback(this); mNormalIconDrawable = normalDrawable.mutate(); mNormalIconDrawable.setCallback(this); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.setCallback(null); mSelectedIconDrawable = null; } if(mDynamicChangeIconColor){ DrawableCompat.setTint(mNormalIconDrawable, QMUIColorHelper.computeColor(normalColor, selectColor, mCurrentSelectFraction)); } invalidateSelf(); } @Override public int getIntrinsicWidth() { return mNormalIconDrawable.getIntrinsicWidth(); } @Override public int getIntrinsicHeight() { return mNormalIconDrawable.getIntrinsicHeight(); } @Override public void setAlpha(int alpha) { // not used } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { // not used } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } /** * set the select faction for QMUITabIcon, value must be in [0, 1] * * @param fraction muse be in [0, 1] */ public void setSelectFraction(float fraction, int color) { fraction = QMUILangHelper.constrain(fraction, 0f, 1f); mCurrentSelectFraction = fraction; if (mSelectedIconDrawable == null) { if(mDynamicChangeIconColor){ DrawableCompat.setTint(mNormalIconDrawable, color); } } else { int normalAlpha = (int) (255 * (1 - fraction)); mNormalIconDrawable.setAlpha(normalAlpha); mSelectedIconDrawable.setAlpha(255 - normalAlpha); } invalidateSelf(); } @Override public void draw(@NonNull Canvas canvas) { mNormalIconDrawable.draw(canvas); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.draw(canvas); } } @Override protected void onBoundsChange(Rect bounds) { mNormalIconDrawable.setBounds(bounds); if (mSelectedIconDrawable != null) { mSelectedIconDrawable.setBounds(bounds); } } @Override public void invalidateDrawable(@NonNull Drawable who) { Callback callback = getCallback(); if(callback != null){ callback.invalidateDrawable(who); } } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { Callback callback = getCallback(); if(callback != null){ callback.scheduleDrawable(who, what, when); } } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { Callback callback = getCallback(); if(callback != null){ callback.unscheduleDrawable(who, what); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabIndicator.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.widget.tab; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.View; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIDrawableHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; public class QMUITabIndicator { /** * the height of indicator */ private int mIndicatorHeight; /** * is indicator layout in top of QMUITabSegment? */ private boolean mIndicatorTop = false; /** * use a drawable to present the indicator */ private @Nullable Drawable mIndicatorDrawable; /** * the width of indicator changed when toggle to different tab */ private boolean mIsIndicatorWidthFollowContent = true; /** * indicator rect, draw directly */ private Rect mIndicatorRect = null; /** * indicator paint, draw directly */ private Paint mIndicatorPaint = null; private int mFixedColorAttr = 0; private boolean mShouldReGetFixedColor = true; private int mFixedColor = 0; public QMUITabIndicator(int indicatorHeight, boolean indicatorTop, boolean isIndicatorWidthFollowContent){ this(indicatorHeight, indicatorTop, isIndicatorWidthFollowContent, 0); } public QMUITabIndicator(int indicatorHeight, boolean indicatorTop, boolean isIndicatorWidthFollowContent, int fixedColorAttr) { mIndicatorHeight = indicatorHeight; mIndicatorTop = indicatorTop; mIsIndicatorWidthFollowContent = isIndicatorWidthFollowContent; mFixedColorAttr = fixedColorAttr; } public QMUITabIndicator(@NonNull Drawable drawable, boolean indicatorTop, boolean isIndicatorWidthFollowContent){ this(drawable, indicatorTop, isIndicatorWidthFollowContent, 0); } public QMUITabIndicator(@NonNull Drawable drawable, boolean indicatorTop, boolean isIndicatorWidthFollowContent, int fixedColorAttr) { mIndicatorDrawable = drawable; mIndicatorHeight = drawable.getIntrinsicHeight(); mIndicatorTop = indicatorTop; mIsIndicatorWidthFollowContent = isIndicatorWidthFollowContent; mFixedColorAttr = fixedColorAttr; } public boolean isIndicatorWidthFollowContent() { return mIsIndicatorWidthFollowContent; } public boolean isIndicatorTop() { return mIndicatorTop; } @Deprecated protected void updateInfo(int left, int width, int color){ if (mIndicatorRect == null) { mIndicatorRect = new Rect(left, 0, left + width, 0); } else { mIndicatorRect.left = left; mIndicatorRect.right = left + width; } if(mFixedColorAttr == 0){ updateColor(color); } } protected void updateInfo(int left, int width, int color, float offsetPercent) { updateInfo(left, width, color); } protected void updateColor(int color){ if (mIndicatorDrawable != null) { DrawableCompat.setTint(mIndicatorDrawable, color); } else { if (mIndicatorPaint == null) { mIndicatorPaint = new Paint(); mIndicatorPaint.setStyle(Paint.Style.FILL); } mIndicatorPaint.setColor(color); } } protected void draw(@NonNull View hostView, @NonNull Canvas canvas, int viewTop, int viewBottom) { if (mIndicatorRect != null) { if(mFixedColorAttr != 0 && mShouldReGetFixedColor){ mShouldReGetFixedColor = false; mFixedColor = QMUISkinHelper.getSkinColor(hostView, mFixedColorAttr); updateColor(mFixedColor); } if (mIndicatorTop) { mIndicatorRect.top = viewTop; mIndicatorRect.bottom = mIndicatorRect.top + mIndicatorHeight; } else { mIndicatorRect.bottom = viewBottom; mIndicatorRect.top = mIndicatorRect.bottom - mIndicatorHeight; } if (mIndicatorDrawable != null) { mIndicatorDrawable.setBounds(mIndicatorRect); mIndicatorDrawable.draw(canvas); } else { canvas.drawRect(mIndicatorRect, mIndicatorPaint); } } } protected void handleSkinChange(@NonNull QMUISkinManager manager, int skinIndex, @NonNull Resources.Theme theme, @Nullable QMUITab selectedTab){ mShouldReGetFixedColor = true; if(selectedTab != null && mFixedColorAttr == 0){ updateColor( selectedTab.selectedColorAttr == 0 ? selectedTab.selectColor : QMUIResHelper.getAttrColor(theme,selectedTab.selectedColorAttr)); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment.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.widget.tab; import android.content.Context; import android.database.DataSetObserver; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import java.lang.ref.WeakReference; /** * 在 {@link QMUIBasicTabSegment} 的基础上添加与 {@link ViewPager} 的联动使用 */ public class QMUITabSegment extends QMUIBasicTabSegment { private static final String TAG = "QMUITabSegment"; /** * the scrollState of ViewPager */ private int mViewPagerScrollState = ViewPager.SCROLL_STATE_IDLE; private ViewPager mViewPager; private PagerAdapter mPagerAdapter; private DataSetObserver mPagerAdapterObserver; private ViewPager.OnPageChangeListener mOnPageChangeListener; private OnTabSelectedListener mViewPagerSelectedListener; private AdapterChangeListener mAdapterChangeListener; public QMUITabSegment(Context context) { super(context); } public QMUITabSegment(Context context, AttributeSet attrs) { super(context, attrs); } public QMUITabSegment(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected boolean needPreventEvent() { return mViewPagerScrollState != ViewPager.SCROLL_STATE_IDLE; } @Override public void notifyDataChanged() { super.notifyDataChanged(); populateFromPagerAdapter(false); } public void notifyDataRefreshed(){ super.notifyDataChanged(); } public void setupWithViewPager(@Nullable ViewPager viewPager) { setupWithViewPager(viewPager, true); } public void setupWithViewPager(@Nullable ViewPager viewPager, boolean useAdapterTitle) { setupWithViewPager(viewPager, useAdapterTitle, true); } /** * associate QMUITabSegment with a {@link ViewPager} * * @param viewPager the ViewPager to associate * @param useAdapterTitle populate the tab with viewPager.adapter.getTitle * @param autoRefresh refresh QMUITabSegment when viewPager.adapter changed. */ public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean useAdapterTitle, boolean autoRefresh) { if (mViewPager != null) { // If we've already been setup with a ViewPager, remove us from it if (mOnPageChangeListener != null) { mViewPager.removeOnPageChangeListener(mOnPageChangeListener); } if (mAdapterChangeListener != null) { mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener); } } if (mViewPagerSelectedListener != null) { // If we already have a tab selected listener for the ViewPager, remove it removeOnTabSelectedListener(mViewPagerSelectedListener); mViewPagerSelectedListener = null; } if (viewPager != null) { mViewPager = viewPager; // Add our custom OnPageChangeListener to the ViewPager if (mOnPageChangeListener == null) { mOnPageChangeListener = new TabLayoutOnPageChangeListener(this); } viewPager.addOnPageChangeListener(mOnPageChangeListener); // Now we'll add a tab selected listener to set ViewPager's current item mViewPagerSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); addOnTabSelectedListener(mViewPagerSelectedListener); final PagerAdapter adapter = viewPager.getAdapter(); if (adapter != null) { // Now we'll populate ourselves from the pager adapter, adding an observer if // autoRefresh is enabled setPagerAdapter(adapter, useAdapterTitle, autoRefresh); } // Add a listener so that we're notified of any adapter changes if (mAdapterChangeListener == null) { mAdapterChangeListener = new AdapterChangeListener(useAdapterTitle); } mAdapterChangeListener.setAutoRefresh(autoRefresh); viewPager.addOnAdapterChangeListener(mAdapterChangeListener); } else { // We've been given a null ViewPager so we need to clear out the internal state, // listeners and observers mViewPager = null; setPagerAdapter(null, false, false); } } private void setViewPagerScrollState(int state) { mViewPagerScrollState = state; if (mViewPagerScrollState == ViewPager.SCROLL_STATE_IDLE) { if (mPendingSelectedIndex != NO_POSITION && mSelectAnimator == null) { selectTab(mPendingSelectedIndex, true, false); mPendingSelectedIndex = NO_POSITION; } } } void populateFromPagerAdapter(boolean useAdapterTitle) { if (mPagerAdapter == null) { if (useAdapterTitle) { reset(); } return; } final int adapterCount = mPagerAdapter.getCount(); if (useAdapterTitle) { reset(); for (int i = 0; i < adapterCount; i++) { addTab(mTabBuilder.setText(mPagerAdapter.getPageTitle(i)).build(getContext())); } super.notifyDataChanged(); } if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); selectTab(curItem, true, false); } } void setPagerAdapter(@Nullable final PagerAdapter adapter, boolean useAdapterTitle, final boolean addObserver) { if (mPagerAdapter != null && mPagerAdapterObserver != null) { // If we already have a PagerAdapter, unregister our observer mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); } mPagerAdapter = adapter; if (addObserver && adapter != null) { // Register our observer on the new adapter if (mPagerAdapterObserver == null) { mPagerAdapterObserver = new PagerAdapterObserver(useAdapterTitle); } adapter.registerDataSetObserver(mPagerAdapterObserver); } // Finally make sure we reflect the new adapter populateFromPagerAdapter(useAdapterTitle); } public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { private final WeakReference mTabSegmentRef; public TabLayoutOnPageChangeListener(QMUITabSegment tabSegment) { mTabSegmentRef = new WeakReference<>(tabSegment); } @Override public void onPageScrollStateChanged(final int state) { final QMUITabSegment tabSegment = mTabSegmentRef.get(); if (tabSegment != null) { tabSegment.setViewPagerScrollState(state); } } @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { final QMUITabSegment tabSegment = mTabSegmentRef.get(); if (tabSegment != null) { tabSegment.updateIndicatorPosition(position, positionOffset); } } @Override public void onPageSelected(final int position) { final QMUITabSegment tabSegment = mTabSegmentRef.get(); if (tabSegment != null && tabSegment.mPendingSelectedIndex != NO_POSITION) { tabSegment.mPendingSelectedIndex = position; return; } if (tabSegment != null && tabSegment.getSelectedIndex() != position && position < tabSegment.getTabCount()) { tabSegment.selectTab(position, true, false); } } } private static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener { private final ViewPager mViewPager; public ViewPagerOnTabSelectedListener(ViewPager viewPager) { mViewPager = viewPager; } @Override public void onTabSelected(int index) { mViewPager.setCurrentItem(index, false); } @Override public void onTabUnselected(int index) { } @Override public void onTabReselected(int index) { } @Override public void onDoubleTap(int index) { } } private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener { private boolean mAutoRefresh; private final boolean mUseAdapterTitle; AdapterChangeListener(boolean useAdapterTitle) { mUseAdapterTitle = useAdapterTitle; } @Override public void onAdapterChanged(@NonNull ViewPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { if (mViewPager == viewPager) { setPagerAdapter(newAdapter, mUseAdapterTitle, mAutoRefresh); } } void setAutoRefresh(boolean autoRefresh) { mAutoRefresh = autoRefresh; } } private class PagerAdapterObserver extends DataSetObserver { private final boolean mUseAdapterTitle; PagerAdapterObserver(boolean useAdapterTitle) { mUseAdapterTitle = useAdapterTitle; } @Override public void onChanged() { populateFromPagerAdapter(mUseAdapterTitle); } @Override public void onInvalidated() { populateFromPagerAdapter(mUseAdapterTitle); } } /** * Please use QMUIBasicTabSegment.OnTabClickListener for a replacement */ @Deprecated public interface OnTabClickListener extends QMUIBasicTabSegment.OnTabClickListener{ } /** * Please use QMUIBasicTabSegment.OnTabSelectedListener for a replacement */ @Deprecated public interface OnTabSelectedListener extends QMUIBasicTabSegment.OnTabSelectedListener{ } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabSegment2.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.widget.tab; import android.content.Context; import android.util.AttributeSet; import androidx.annotation.Nullable; import androidx.viewpager2.widget.ViewPager2; import java.lang.ref.WeakReference; /** * 在 {@link QMUIBasicTabSegment} 的基础上添加与 {@link ViewPager2} 的联动使用 */ public class QMUITabSegment2 extends QMUIBasicTabSegment { private static final String TAG = "QMUITabSegment"; /** * the scrollState of ViewPager */ private int mViewPagerScrollState = ViewPager2.SCROLL_STATE_IDLE; private ViewPager2 mViewPager; private ViewPager2.OnPageChangeCallback mOnPageChangeListener; private OnTabSelectedListener mViewPagerSelectedListener; public QMUITabSegment2(Context context) { super(context); } public QMUITabSegment2(Context context, AttributeSet attrs) { super(context, attrs); } public QMUITabSegment2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected boolean needPreventEvent() { return mViewPagerScrollState != ViewPager2.SCROLL_STATE_IDLE; } /** * associate QMUITabSegment2 with a {@link ViewPager2} * * @param viewPager the ViewPager2 to associate */ public void setupWithViewPager(@Nullable final ViewPager2 viewPager) { if (mViewPager != null) { if (mOnPageChangeListener != null) { mViewPager.unregisterOnPageChangeCallback(mOnPageChangeListener); } } if (mViewPagerSelectedListener != null) { removeOnTabSelectedListener(mViewPagerSelectedListener); mViewPagerSelectedListener = null; } if (viewPager != null) { mViewPager = viewPager; if (mOnPageChangeListener == null) { mOnPageChangeListener = new TabLayoutOnPageChangeListener(this); } viewPager.registerOnPageChangeCallback(mOnPageChangeListener); mViewPagerSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); addOnTabSelectedListener(mViewPagerSelectedListener); final int curItem = mViewPager.getCurrentItem(); selectTab(curItem, true, false); } else { mViewPager = null; } } private void setViewPagerScrollState(int state) { mViewPagerScrollState = state; if (mViewPagerScrollState == ViewPager2.SCROLL_STATE_IDLE) { if (mPendingSelectedIndex != NO_POSITION && mSelectAnimator == null) { selectTab(mPendingSelectedIndex, true, false); mPendingSelectedIndex = NO_POSITION; } } } public static class TabLayoutOnPageChangeListener extends ViewPager2.OnPageChangeCallback { private final WeakReference mTabSegmentRef; public TabLayoutOnPageChangeListener(QMUITabSegment2 tabSegment) { mTabSegmentRef = new WeakReference<>(tabSegment); } @Override public void onPageScrollStateChanged(final int state) { final QMUITabSegment2 tabSegment = mTabSegmentRef.get(); if (tabSegment != null) { tabSegment.setViewPagerScrollState(state); } } @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { final QMUITabSegment2 tabSegment = mTabSegmentRef.get(); if (tabSegment != null) { tabSegment.updateIndicatorPosition(position, positionOffset); } } @Override public void onPageSelected(final int position) { final QMUITabSegment2 tabSegment = mTabSegmentRef.get(); if (tabSegment != null && tabSegment.mPendingSelectedIndex != NO_POSITION) { tabSegment.mPendingSelectedIndex = position; return; } if (tabSegment != null && tabSegment.getSelectedIndex() != position && position < tabSegment.getTabCount()) { tabSegment.selectTab(position, true, false); } } } private static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener { private final ViewPager2 mViewPager; public ViewPagerOnTabSelectedListener(ViewPager2 viewPager) { mViewPager = viewPager; } @Override public void onTabSelected(int index) { mViewPager.setCurrentItem(index, false); } @Override public void onTabUnselected(int index) { } @Override public void onTabReselected(int index) { } @Override public void onDoubleTap(int index) { } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/tab/QMUITabView.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.widget.tab; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SimpleArrayMap; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.skin.IQMUISkinHandlerView; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.defaultAttr.QMUISkinSimpleDefaultAttrProvider; import com.qmuiteam.qmui.util.QMUICollapsingTextHelper; import com.qmuiteam.qmui.util.QMUIColorHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import org.jetbrains.annotations.NotNull; public class QMUITabView extends FrameLayout implements IQMUISkinHandlerView { private static final String TAG = "QMUITabView"; private QMUITab mTab; private QMUICollapsingTextHelper mCollapsingTextHelper; private Interpolator mPositionInterpolator; private GestureDetector mGestureDetector; private Callback mCallback; private float mCurrentIconLeft = 0; private float mCurrentIconTop = 0; private float mCurrentTextLeft = 0; private float mCurrentTextTop = 0; private float mCurrentIconWidth = 0; private float mCurrentIconHeight = 0; private float mCurrentTextWidth = 0; private float mCurrentTextHeight = 0; private float mNormalIconLeft = 0; private float mNormalIconTop = 0; private float mNormalTextLeft = 0; private float mNormalTextTop = 0; private float mSelectedIconLeft = 0; private float mSelectedIconTop = 0; private float mSelectedTextLeft = 0; private float mSelectedTextTop = 0; private float mSelectFraction = 0f; private QMUIRoundButton mSignCountView; public QMUITabView(@NonNull Context context) { super(context); // 使得每个tab可被诸如TalkBack等屏幕阅读器聚焦 // 这样视力受损用户(如盲人、低、弱视力)就能与tab交互 this.setFocusable(true); this.setFocusableInTouchMode(true); setWillNotDraw(false); mCollapsingTextHelper = new QMUICollapsingTextHelper(this, 1f); mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { if (mCallback != null) { mCallback.onDoubleClick(QMUITabView.this); return true; } return false; } @Override public boolean onSingleTapUp(MotionEvent e) { if (mCallback != null) { mCallback.onClick(QMUITabView.this); return false; } return false; } @Override public boolean onDown(MotionEvent e) { return mCallback != null; } @Override public void onLongPress(MotionEvent e) { if (mCallback != null) { mCallback.onLongClick(QMUITabView.this); } } }); } public void setCallback(Callback callback) { mCallback = callback; } public void setPositionInterpolator(Interpolator positionInterpolator) { mPositionInterpolator = positionInterpolator; mCollapsingTextHelper.setPositionInterpolator(positionInterpolator); } @Override public boolean onTouchEvent(MotionEvent event) { return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event); } public void bind(QMUITab tab) { mCollapsingTextHelper.setTextSize(tab.normalTextSize, tab.selectedTextSize, false); mCollapsingTextHelper.setTypeface(tab.normalTypeface, tab.selectedTypeface, false); mCollapsingTextHelper.setTypefaceUpdateAreaPercent(tab.typefaceUpdateAreaPercent); int gravity = Gravity.LEFT | Gravity.TOP; mCollapsingTextHelper.setGravity(gravity, gravity, false); mCollapsingTextHelper.setText(tab.getText()); mTab = tab; if(tab.tabIcon != null){ tab.tabIcon.setCallback(this); } boolean hasRedPoint = mTab.signCount == QMUITab.RED_POINT_SIGN_COUNT; boolean hasSignCount = mTab.signCount > 0; if (hasRedPoint || hasSignCount) { ensureSignCountView(getContext()); FrameLayout.LayoutParams signCountLp = (FrameLayout.LayoutParams) mSignCountView.getLayoutParams(); if (hasSignCount) { mSignCountView.setText( QMUILangHelper.formatNumberToLimitedDigits(mTab.signCount, mTab.signCountDigits)); mSignCountView.setMinWidth(QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_tab_sign_count_view_min_size_with_text)); signCountLp.width = ViewGroup.LayoutParams.WRAP_CONTENT; signCountLp.height = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_tab_sign_count_view_min_size_with_text); } else { mSignCountView.setText(null); int redPointSize = QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_tab_sign_count_view_min_size); signCountLp.width = redPointSize; signCountLp.height = redPointSize; } mSignCountView.setLayoutParams(signCountLp); mSignCountView.setVisibility(View.VISIBLE); } else { if (mSignCountView != null) { mSignCountView.setVisibility(View.GONE); } } updateSkinInfo(tab); requestLayout(); setContentDescription(tab.getDescription()); } public float getSelectFraction() { return mSelectFraction; } public void setSelectFraction(float fraction) { fraction = QMUILangHelper.constrain(fraction, 0f, 1f); mSelectFraction = fraction; QMUITabIcon tabIcon = mTab.getTabIcon(); if (tabIcon != null) { tabIcon.setSelectFraction(fraction, QMUIColorHelper.computeColor(mTab.getNormalColor(this), mTab.getSelectColor(this), fraction)); } updateCurrentInfo(fraction); mCollapsingTextHelper.setExpansionFraction(1 - fraction); if (mSignCountView != null) { Point point = calculateSignCountLayoutPosition(); int x = point.x, y = point.y; if (point.x + mSignCountView.getMeasuredWidth() > getMeasuredWidth()) { x = getMeasuredWidth() - mSignCountView.getMeasuredWidth(); } if (point.y - mSignCountView.getMeasuredHeight() < 0) { y = mSignCountView.getMeasuredHeight(); } ViewCompat.offsetLeftAndRight(mSignCountView, x - mSignCountView.getLeft()); ViewCompat.offsetTopAndBottom(mSignCountView, y - mSignCountView.getBottom()); } } private void updateCurrentInfo(float fraction) { mCurrentIconLeft = QMUICollapsingTextHelper.lerp( mNormalIconLeft, mSelectedIconLeft, fraction, mPositionInterpolator); mCurrentIconTop = QMUICollapsingTextHelper.lerp( mNormalIconTop, mSelectedIconTop, fraction, mPositionInterpolator); int normalIconWidth = mTab.getNormalTabIconWidth(); int normalIconHeight = mTab.getNormalTabIconHeight(); float selectedScale = mTab.getSelectedTabIconScale(); mCurrentIconWidth = QMUICollapsingTextHelper.lerp(normalIconWidth, normalIconWidth * selectedScale, fraction, mPositionInterpolator); mCurrentIconHeight = QMUICollapsingTextHelper.lerp(normalIconHeight, normalIconHeight * selectedScale, fraction, mPositionInterpolator); mCurrentTextLeft = QMUICollapsingTextHelper.lerp( mNormalTextLeft, mSelectedTextLeft, fraction, mPositionInterpolator); mCurrentTextTop = QMUICollapsingTextHelper.lerp( mNormalTextTop, mSelectedTextTop, fraction, mPositionInterpolator); float normalTextWidth = mCollapsingTextHelper.getCollapsedTextWidth(); float normalTextHeight = mCollapsingTextHelper.getCollapsedTextHeight(); float selectedTextWidth = mCollapsingTextHelper.getExpandedTextWidth(); float selectedTextHeight = mCollapsingTextHelper.getExpandedTextHeight(); mCurrentTextWidth = QMUICollapsingTextHelper.lerp( normalTextWidth, selectedTextWidth, fraction, mPositionInterpolator); mCurrentTextHeight = QMUICollapsingTextHelper.lerp( normalTextHeight, selectedTextHeight, fraction, mPositionInterpolator); } public int getContentViewWidth() { if (mTab == null) { return 0; } float textWidth = mCollapsingTextHelper.getExpandedTextWidth(); if (mTab.getTabIcon() == null) { return (int) (textWidth + 0.5); } int iconPosition = mTab.getIconPosition(); float iconWidth = mTab.getNormalTabIconWidth() * mTab.getSelectedTabIconScale(); if (iconPosition == QMUITab.ICON_POSITION_BOTTOM || iconPosition == QMUITab.ICON_POSITION_TOP) { return (int) (Math.max(iconWidth, textWidth) + 0.5); } return (int) (iconWidth + textWidth + mTab.getIconTextGap() + 0.5); } public int getContentViewLeft() { if (mTab == null) { return 0; } if (mTab.getTabIcon() == null) { return (int) (mSelectedTextLeft + 0.5); } int iconPosition = mTab.getIconPosition(); if (iconPosition == QMUITab.ICON_POSITION_BOTTOM || iconPosition == QMUITab.ICON_POSITION_TOP) { return (int) Math.min(mSelectedTextLeft, mSelectedIconLeft + 0.5); } else if (iconPosition == QMUITab.ICON_POSITION_LEFT) { return (int) (mSelectedIconLeft + 0.5); } else { return (int) (mSelectedTextLeft + 0.5); } } private QMUIRoundButton ensureSignCountView(Context context) { if (mSignCountView == null) { mSignCountView = createSignCountView(context); FrameLayout.LayoutParams signCountLp; if (mSignCountView.getLayoutParams() != null) { signCountLp = new FrameLayout.LayoutParams(mSignCountView.getLayoutParams()); } else { signCountLp = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } addView(mSignCountView, signCountLp); } return mSignCountView; } protected QMUIRoundButton createSignCountView(Context context) { QMUIRoundButton btn = new QMUIRoundButton( context, null, R.attr.qmui_tab_sign_count_view); QMUISkinSimpleDefaultAttrProvider skinProvider = new QMUISkinSimpleDefaultAttrProvider(); skinProvider.setDefaultSkinAttr( QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_tab_sign_count_view_bg_color); skinProvider.setDefaultSkinAttr( QMUISkinValueBuilder.TEXT_COLOR, R.attr.qmui_skin_support_tab_sign_count_view_text_color); btn.setTag(R.id.qmui_skin_default_attr_provider, skinProvider); return btn; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mTab == null) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); onMeasureTab(widthSize, heightSize); int useWidthMeasureSpec = widthMeasureSpec; int useHeightMeasureSpec = heightMeasureSpec; QMUITabIcon icon = mTab.getTabIcon(); int iconPosition = mTab.getIconPosition(); if (widthMode == MeasureSpec.AT_MOST) { if (icon == null) { widthSize = (int) mCollapsingTextHelper.getExpandedTextWidth(); } else if (iconPosition == QMUITab.ICON_POSITION_BOTTOM || iconPosition == QMUITab.ICON_POSITION_TOP) { widthSize = (int) Math.max( mTab.getNormalTabIconWidth() * mTab.getSelectedTabIconScale(), mCollapsingTextHelper.getExpandedTextWidth()); } else { widthSize = (int) (mCollapsingTextHelper.getExpandedTextWidth() + mTab.getIconTextGap() + mTab.getNormalTabIconWidth() * mTab.getSelectedTabIconScale()); } if(mSignCountView != null && mSignCountView.getVisibility() != View.GONE){ mSignCountView.measure(0, 0); widthSize = Math.max(widthSize, widthSize + mSignCountView.getMeasuredWidth() + mTab.signCountHorizontalOffset); } useWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); } if (heightMode == MeasureSpec.AT_MOST) { if (icon == null) { heightSize = (int) mCollapsingTextHelper.getExpandedTextHeight(); } else if (iconPosition == QMUITab.ICON_POSITION_LEFT || iconPosition == QMUITab.ICON_POSITION_RIGHT) { heightSize = (int) Math.max( mTab.getNormalTabIconHeight() * mTab.getSelectedTabIconScale(), mCollapsingTextHelper.getExpandedTextWidth()); } else { heightSize = (int) (mCollapsingTextHelper.getExpandedTextHeight() + mTab.getIconTextGap() + mTab.getNormalTabIconHeight() * mTab.getSelectedTabIconScale()); } useHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); } super.onMeasure(useWidthMeasureSpec, useHeightMeasureSpec); } protected void onMeasureTab(int widthSize, int heightSize) { int textWidth = widthSize, textHeight = heightSize; QMUITabIcon icon = mTab.getTabIcon(); if (icon != null && !mTab.isAllowIconDrawOutside()) { float iconWidth = mTab.getNormalTabIconWidth() * mTab.selectedTabIconScale; float iconHeight = mTab.getNormalTabIconHeight() * mTab.selectedTabIconScale; int iconPosition = mTab.iconPosition; if (iconPosition == QMUITab.ICON_POSITION_TOP || iconPosition == QMUITab.ICON_POSITION_BOTTOM) { textHeight -= iconHeight - mTab.getIconTextGap(); } else { textWidth -= iconWidth - mTab.getIconTextGap(); } } mCollapsingTextHelper.setCollapsedBounds(0, 0, textWidth, textHeight); mCollapsingTextHelper.setExpandedBounds(0, 0, textWidth, textHeight); mCollapsingTextHelper.calculateBaseOffsets(); } @Override protected final void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); onLayoutTab(right - left, bottom - top); onLayoutSignCount(right - left, bottom - top); } protected void onLayoutSignCount(int width, int height) { if (mSignCountView != null && mTab != null) { Point point = calculateSignCountLayoutPosition(); int x = point.x, y = point.y; if (point.x + mSignCountView.getMeasuredWidth() > width) { x = width - mSignCountView.getMeasuredWidth(); } if (point.y - mSignCountView.getMeasuredHeight() < 0) { y = mSignCountView.getMeasuredHeight(); } mSignCountView.layout(x, y - mSignCountView.getMeasuredHeight(), x + mSignCountView.getMeasuredWidth(), y); } } private Point calculateSignCountLayoutPosition() { QMUITabIcon icon = mTab.getTabIcon(); int anchorLeft, anchorTop; int iconPosition = mTab.getIconPosition(); if (icon == null || iconPosition == QMUITab.ICON_POSITION_BOTTOM || iconPosition == QMUITab.ICON_POSITION_LEFT) { anchorLeft = (int) (mCurrentTextLeft + mCurrentTextWidth); anchorTop = (int) (mCurrentTextTop); } else { anchorLeft = (int) (mCurrentIconLeft + mCurrentIconWidth); anchorTop = (int) (mCurrentIconTop); } Point point = new Point(anchorLeft, anchorTop); int verticalAlign = mTab.signCountVerticalAlign; int verticalOffset = mTab.signCountVerticalOffset; if(verticalAlign == QMUITab.SIGN_COUNT_VERTICAL_ALIGN_TOP_TO_CONTENT_TOP){ point.offset(mTab.signCountHorizontalOffset, verticalOffset + mSignCountView.getMeasuredHeight()); }else if(verticalAlign == QMUITab.SIGN_COUNT_VERTICAL_ALIGN_MIDDLE_TO_CONTENT){ point.y = getMeasuredHeight() - (getMeasuredHeight() - mSignCountView.getMeasuredHeight()) / 2; point.offset(mTab.signCountHorizontalOffset, verticalOffset); }else { point.offset(mTab.signCountHorizontalOffset, verticalOffset); } return point; } protected void onLayoutTab(int width, int height) { if (mTab == null) { return; } mCollapsingTextHelper.calculateCurrentOffsets(); QMUITabIcon icon = mTab.getTabIcon(); float normalTextWidth = mCollapsingTextHelper.getCollapsedTextWidth(); float normalTextHeight = mCollapsingTextHelper.getCollapsedTextHeight(); float selectedTextWidth = mCollapsingTextHelper.getExpandedTextWidth(); float selectedTextHeight = mCollapsingTextHelper.getExpandedTextHeight(); if (icon == null) { mNormalIconLeft = mNormalIconTop = mSelectedIconLeft = mSelectedIconTop = 0; switch (mTab.gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: mNormalTextTop = height - normalTextHeight; mSelectedTextTop = height - selectedTextHeight; break; case Gravity.TOP: mNormalTextTop = 0; mSelectedTextTop = 0; break; case Gravity.CENTER_VERTICAL: default: mNormalTextTop = (height - normalTextHeight) / 2; mSelectedTextTop = (height - selectedTextHeight) / 2; break; } switch (mTab.gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.RIGHT: mNormalTextLeft = width - normalTextWidth; mSelectedTextLeft = width - selectedTextWidth; break; case Gravity.LEFT: mNormalTextLeft = 0; mSelectedTextLeft = 0; break; case Gravity.CENTER_HORIZONTAL: default: mNormalTextLeft = (width - normalTextWidth) / 2; mSelectedTextLeft = (width - selectedTextWidth) / 2; break; } } else { int gap = mTab.getIconTextGap(); int iconPosition = mTab.iconPosition; // icon float normalIconWidth = mTab.getNormalTabIconWidth(); float normalIconHeight = mTab.getNormalTabIconHeight(); float selectedIconWidth = normalIconWidth * mTab.getSelectedTabIconScale(); float selectedIconHeight = normalIconHeight * mTab.getSelectedTabIconScale(); // total size float normalTotalWidth = normalTextWidth + gap + normalIconWidth; float normalTotalHeight = normalTextHeight + gap + normalIconHeight; float selectedTotalWidth = selectedTextWidth + gap + selectedIconWidth; float selectedTotalHeight = selectedTextHeight + gap + selectedIconHeight; if (iconPosition == QMUITab.ICON_POSITION_TOP || iconPosition == QMUITab.ICON_POSITION_BOTTOM) { switch (mTab.gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.RIGHT: mNormalIconLeft = width - normalIconWidth; mNormalTextLeft = width - normalTextWidth; mSelectedIconLeft = width - selectedIconWidth; mSelectedTextLeft = width - selectedTextWidth; break; case Gravity.LEFT: mNormalIconLeft = 0; mNormalTextLeft = 0; mSelectedIconLeft = 0; mSelectedTextLeft = 0; break; case Gravity.CENTER_HORIZONTAL: default: mNormalIconLeft = (width - normalIconWidth) / 2; mNormalTextLeft = (width - normalTextWidth) / 2; mSelectedIconLeft = (width - selectedIconWidth) / 2; mSelectedTextLeft = (width - selectedTextWidth) / 2; break; } switch (mTab.gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: if (iconPosition == QMUITab.ICON_POSITION_TOP) { mNormalTextTop = height - normalTextHeight; mSelectedTextTop = height - selectedTextHeight; mNormalIconTop = mNormalTextTop - gap - normalIconHeight; mSelectedIconTop = mSelectedTextTop - gap - selectedIconHeight; } else { mNormalIconTop = height - normalIconHeight; mSelectedIconTop = height - selectedIconHeight; mNormalTextTop = mNormalIconTop - gap - normalTextHeight; mSelectedTextTop = mSelectedIconTop - gap - selectedTextHeight; } break; case Gravity.TOP: if (iconPosition == QMUITab.ICON_POSITION_TOP) { mNormalIconTop = 0; mSelectedIconTop = 0; mNormalTextTop = normalIconHeight + gap; mSelectedTextTop = selectedIconHeight + gap; } else { mNormalTextTop = 0; mSelectedTextTop = 0; mNormalIconTop = normalTextHeight + gap; mSelectedIconTop = selectedTextHeight + gap; } break; case Gravity.CENTER_VERTICAL: default: // if the space is not enough, keep text if (iconPosition == QMUITab.ICON_POSITION_TOP) { // normal if (normalTotalHeight >= height) { mNormalIconTop = height - normalTotalHeight; } else { mNormalIconTop = (height - normalTotalHeight) / 2; } mNormalTextTop = mNormalIconTop + gap + normalIconHeight; // selected if (selectedTotalHeight >= height) { mSelectedIconTop = height - selectedTotalHeight; } else { mSelectedIconTop = (height - selectedTotalHeight) / 2; } mSelectedTextTop = mSelectedIconTop + gap + selectedIconHeight; } else { // normal if (normalTotalHeight >= height) { mNormalTextTop = 0; } else { mNormalTextTop = (height - normalTotalHeight) / 2; } mNormalIconTop = mNormalTextTop + gap + normalTextHeight; // selected if (selectedTotalHeight >= height) { mNormalTextTop = 0; } else { mNormalTextTop = (height - selectedTotalHeight) / 2; } mNormalIconTop = mNormalTextTop + gap + selectedTextHeight; } break; } } else { switch (mTab.gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: mNormalIconTop = height - normalIconHeight; mNormalTextTop = height - normalTextHeight; mSelectedIconTop = height - selectedIconHeight; mSelectedTextTop = height - selectedTextHeight; break; case Gravity.TOP: mNormalIconTop = 0; mNormalTextTop = 0; mSelectedIconTop = 0; mSelectedTextTop = 0; break; case Gravity.CENTER_VERTICAL: default: mNormalIconTop = (height - normalIconHeight) / 2; mNormalTextTop = (height - normalTextHeight) / 2; mSelectedIconTop = (height - selectedIconHeight) / 2; mSelectedTextTop = (height - selectedTextHeight) / 2; break; } switch (mTab.gravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.RIGHT: if (iconPosition == QMUITab.ICON_POSITION_RIGHT) { mNormalTextLeft = width - normalTotalWidth; mSelectedTextLeft = width - selectedTotalWidth; mNormalIconLeft = width - normalIconWidth; mSelectedIconLeft = width - selectedIconWidth; } else { mNormalIconLeft = width - normalTotalWidth; mSelectedIconLeft = width - selectedTotalWidth; mNormalTextLeft = width - normalTextWidth; mSelectedTextLeft = width - selectedTextWidth; } break; case Gravity.LEFT: if (iconPosition == QMUITab.ICON_POSITION_RIGHT) { mNormalTextLeft = 0; mSelectedTextLeft = 0; mNormalIconLeft = normalTextWidth + gap; mSelectedIconLeft = selectedTextWidth + gap; } else { mNormalIconLeft = 0; mSelectedIconLeft = 0; mNormalTextLeft = normalIconWidth + gap; mSelectedTextLeft = selectedIconWidth + gap; } break; case Gravity.CENTER_HORIZONTAL: default: if (iconPosition == QMUITab.ICON_POSITION_RIGHT) { mNormalTextLeft = (width - normalTotalWidth) / 2; mSelectedTextLeft = (width - selectedTotalWidth) / 2; mNormalIconLeft = mNormalTextLeft + normalTextWidth + gap; mSelectedIconLeft = mSelectedTextLeft + selectedTextWidth + gap; } else { mNormalIconLeft = (width - normalTotalWidth) / 2; mSelectedIconLeft = (width - selectedTotalWidth) / 2; mNormalTextLeft = mNormalIconLeft + normalIconWidth + gap; mSelectedTextLeft = mSelectedIconLeft + selectedIconWidth + gap; } break; } if (iconPosition == QMUITab.ICON_POSITION_LEFT) { // normal if (normalTotalWidth >= width) { mNormalIconLeft = width - normalTotalWidth; } else { mNormalIconLeft = (width - normalTotalWidth) / 2; } mNormalTextLeft = mNormalIconLeft + normalIconWidth + gap; // selected if (selectedTotalWidth >= width) { mSelectedIconLeft = width - selectedTotalWidth; } else { mSelectedIconLeft = (width - selectedTotalWidth) / 2; } mSelectedTextLeft = mSelectedIconLeft + selectedIconWidth + gap; } else { // normal if (normalTotalWidth >= width) { mNormalTextLeft = 0; } else { mNormalTextLeft = (width - normalTotalWidth) / 2; } mNormalIconLeft = mNormalTextLeft + normalTextWidth + gap; // selected if (selectedTotalWidth >= width) { mSelectedTextLeft = 0; } else { mSelectedTextLeft = (width - selectedTotalWidth) / 2; } mSelectedIconLeft = mSelectedTextLeft + selectedTextWidth + gap; } } } updateCurrentInfo(1 - mCollapsingTextHelper.getExpansionFraction()); } @Override public void invalidateDrawable(@NonNull Drawable drawable) { invalidate(); } @Override public final void draw(Canvas canvas) { onDrawTab(canvas); super.draw(canvas); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); // 给每个tab添加文本标签 // 使得TalkBack等屏幕阅读器focus 到 tab上时可将tab的文本通过TTS朗读出来 // 这样视力受损用户(如盲人、低、弱视力)就能和widget交互 info.setContentDescription(mTab.getText()); } protected void onDrawTab(Canvas canvas) { if (mTab == null) { return; } QMUITabIcon icon = mTab.getTabIcon(); if (icon != null) { canvas.save(); canvas.translate(mCurrentIconLeft, mCurrentIconTop); icon.setBounds(0, 0, (int) mCurrentIconWidth, (int) mCurrentIconHeight); icon.draw(canvas); canvas.restore(); } canvas.save(); canvas.translate(mCurrentTextLeft, mCurrentTextTop); mCollapsingTextHelper.draw(canvas); canvas.restore(); } @Override public void handle(@NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme, @Nullable SimpleArrayMap attrs) { if (mTab != null) { updateSkinInfo(mTab); invalidate(); } } private void updateSkinInfo(QMUITab tab) { int normalColor = tab.getNormalColor(this); int selectedColor = tab.getSelectColor(this); mCollapsingTextHelper.setTextColor( ColorStateList.valueOf(normalColor), ColorStateList.valueOf(selectedColor), true); if (tab.tabIcon != null) { if (tab.skinChangeWithTintColor || (tab.skinChangeNormalWithTintColor && tab.skinChangeSelectedWithTintColor)) { tab.tabIcon.tint(normalColor, selectedColor); } else { if(tab.tabIcon.hasSelectedIcon()){ if(tab.skinChangeNormalWithTintColor){ tab.tabIcon.tintNormal(normalColor); }else{ if(tab.normalIconAttr != 0){ Drawable normalIcon = QMUISkinHelper.getSkinDrawable(this, tab.normalIconAttr); if(normalIcon != null){ tab.tabIcon.srcNormal(normalIcon); } } } if(tab.skinChangeSelectedWithTintColor){ tab.tabIcon.tintSelected(normalColor); }else{ if(tab.selectedIconAttr != 0){ Drawable selectedIcon = QMUISkinHelper.getSkinDrawable(this, tab.selectedIconAttr); if(selectedIcon != null){ tab.tabIcon.srcSelected(selectedIcon); } } } }else{ if(tab.skinChangeNormalWithTintColor){ tab.tabIcon.tint(normalColor, selectedColor); }else{ if(tab.normalIconAttr != 0){ Drawable normalIcon = QMUISkinHelper.getSkinDrawable(this, tab.normalIconAttr); if(normalIcon != null){ tab.tabIcon.src(normalIcon, normalColor, selectedColor); } } } } } } } public interface Callback { void onClick(QMUITabView view); void onDoubleClick(QMUITabView view); void onLongClick(QMUITabView view); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/textview/ISpanTouchFix.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.widget.textview; /** * @author cginechen * @date 2017-08-07 */ public interface ISpanTouchFix { /** * 记录当前 Touch 事件对应的点是不是点在了 span 上面 */ void setTouchSpanHit(boolean hit); } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUILinkTextView.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.widget.textview; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import androidx.core.content.ContextCompat; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; import com.qmuiteam.qmui.R; import com.qmuiteam.qmui.alpha.QMUIAlphaTextView; import com.qmuiteam.qmui.link.QMUILinkTouchMovementMethod; import com.qmuiteam.qmui.link.QMUILinkify; import com.qmuiteam.qmui.span.QMUIOnSpanClickListener; import java.util.HashSet; import java.util.Set; /** * 使 {@link android.widget.TextView} 能自动识别 URL、电话、邮箱地址。 * 相比于 {@link android.widget.TextView} 使用 {@link android.text.util.Linkify}, * {@link QMUILinkTextView} 有以下特点: *
    *
  • 可以通过 {@link com.qmuiteam.qmui.R.styleable#QMUILinkTextView} 中的属性设置链接的样式
  • *
  • 可以通过 {@link QMUILinkTextView#setOnLinkClickListener(QMUILinkTextView.OnLinkClickListener)} 设置链接的点击事件, * 而不是 {@link android.widget.TextView} 默认的 {@link android.content.Intent} 跳转
  • *
* * @author cginechen * @date 2017-03-17 */ public class QMUILinkTextView extends QMUIAlphaTextView implements QMUIOnSpanClickListener { private static final String TAG = "LinkTextView"; private static final int MSG_CHECK_DOUBLE_TAP_TIMEOUT = 1000; public static int AUTO_LINK_MASK_REQUIRED = QMUILinkify.PHONE_NUMBERS | QMUILinkify.EMAIL_ADDRESSES | QMUILinkify.WEB_URLS; private static Set AUTO_LINK_SCHEME_INTERRUPTED = new HashSet<>(); private CharSequence mOriginText = null; static { AUTO_LINK_SCHEME_INTERRUPTED.add("tel"); AUTO_LINK_SCHEME_INTERRUPTED.add("mailto"); AUTO_LINK_SCHEME_INTERRUPTED.add("http"); AUTO_LINK_SCHEME_INTERRUPTED.add("https"); } /** * 链接文字颜色 */ private ColorStateList mLinkTextColor; /** * 链接背景颜色 */ private ColorStateList mLinkBgColor; private int mAutoLinkMaskCompat; private OnLinkClickListener mOnLinkClickListener; private OnLinkLongClickListener mOnLinkLongClickListener; private long mDownMillis = 0; private static final long TAP_TIMEOUT = 200; // ViewConfiguration.getTapTimeout(); private static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); public QMUILinkTextView(Context context) { this(context, null); mLinkBgColor = null; mLinkTextColor = ContextCompat.getColorStateList(context, R.color.qmui_s_link_color); } public QMUILinkTextView(Context context, ColorStateList linkTextColor, ColorStateList linkBgColor) { this(context, null); mLinkBgColor = linkBgColor; mLinkTextColor = linkTextColor; } public QMUILinkTextView(Context context, AttributeSet attrs) { super(context, attrs); mAutoLinkMaskCompat = getAutoLinkMask() | AUTO_LINK_MASK_REQUIRED; setAutoLinkMask(0); setMovementMethodCompat(QMUILinkTouchMovementMethod.getInstance()); setHighlightColor(Color.TRANSPARENT); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QMUILinkTextView); mLinkBgColor = array.getColorStateList(R.styleable.QMUILinkTextView_qmui_linkBackgroundColor); mLinkTextColor = array.getColorStateList(R.styleable.QMUILinkTextView_qmui_linkTextColor); array.recycle(); if (mOriginText != null) { setText(mOriginText); } setChangeAlphaWhenPress(false); } public void setOnLinkClickListener(OnLinkClickListener onLinkClickListener) { mOnLinkClickListener = onLinkClickListener; } public void setOnLinkLongClickListener(OnLinkLongClickListener onLinkLongClickListener) { mOnLinkLongClickListener = onLinkLongClickListener; } public int getAutoLinkMaskCompat() { return mAutoLinkMaskCompat; } public void setAutoLinkMaskCompat(int mask) { mAutoLinkMaskCompat = mask; } public void addAutoLinkMaskCompat(int mask) { mAutoLinkMaskCompat |= mask; } public void removeAutoLinkMaskCompat(int mask) { mAutoLinkMaskCompat &= ~mask; } public void setLinkColor(ColorStateList linkTextColor) { mLinkTextColor = linkTextColor; } @Override public void setText(CharSequence text, BufferType type) { mOriginText = text; if (!TextUtils.isEmpty(text)) { SpannableStringBuilder builder = new SpannableStringBuilder(text); QMUILinkify.addLinks(builder, mAutoLinkMaskCompat, mLinkTextColor, mLinkBgColor, this); text = builder; } super.setText(text, type); } @Override public boolean onSpanClick(String text) { if (null == text) { Log.w(TAG, "onSpanClick interrupt null text"); return true; } long clickUpTime = (SystemClock.uptimeMillis() - mDownMillis); Log.w(TAG, "onSpanClick clickUpTime: " + clickUpTime); if (mSingleTapConfirmedHandler.hasMessages(MSG_CHECK_DOUBLE_TAP_TIMEOUT)) { disallowOnSpanClickInterrupt(); return true; } if (TAP_TIMEOUT < clickUpTime) { Log.w(TAG, "onSpanClick interrupted because of TAP_TIMEOUT: " + clickUpTime); return true; } String scheme = Uri.parse(text).getScheme(); if (scheme != null) { scheme = scheme.toLowerCase(); } if (AUTO_LINK_SCHEME_INTERRUPTED.contains(scheme)) { long waitTime = DOUBLE_TAP_TIMEOUT - clickUpTime; mSingleTapConfirmedHandler.removeMessages(MSG_CHECK_DOUBLE_TAP_TIMEOUT); Message msg = Message.obtain(); msg.what = MSG_CHECK_DOUBLE_TAP_TIMEOUT; msg.obj = text; mSingleTapConfirmedHandler.sendMessageDelayed(msg, waitTime); return true; } return false; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: boolean hasSingleTap = mSingleTapConfirmedHandler.hasMessages(MSG_CHECK_DOUBLE_TAP_TIMEOUT); Log.w(TAG, "onTouchEvent hasSingleTap: " + hasSingleTap); if (!hasSingleTap) { mDownMillis = SystemClock.uptimeMillis(); } else { Log.w(TAG, "onTouchEvent disallow onSpanClick mSingleTapConfirmedHandler because of DOUBLE TAP"); disallowOnSpanClickInterrupt(); } break; } return super.onTouchEvent(event); } private void disallowOnSpanClickInterrupt() { mSingleTapConfirmedHandler.removeMessages(MSG_CHECK_DOUBLE_TAP_TIMEOUT); mDownMillis = 0; } protected boolean performSpanLongClick(String text) { if (mOnLinkLongClickListener != null) { mOnLinkLongClickListener.onLongClick(text); return true; } return false; } @Override public boolean performLongClick() { int end = getSelectionEnd(); if (end > 0) { String selectStr = getText().subSequence(getSelectionStart(), end).toString(); return performSpanLongClick(selectStr) || super.performLongClick(); } return super.performLongClick(); } private Handler mSingleTapConfirmedHandler = new Handler(Looper.getMainLooper()) { public void handleMessage(android.os.Message msg) { if (MSG_CHECK_DOUBLE_TAP_TIMEOUT != msg.what) { return; } Log.d(TAG, "handleMessage: " + msg.obj); if (msg.obj instanceof String) { String url = (String) msg.obj; if (null != mOnLinkClickListener && !TextUtils.isEmpty(url)) { String schemeUrl = url.toLowerCase(); if (schemeUrl.startsWith("tel:")) { String phoneNumber = Uri.parse(url).getSchemeSpecificPart(); mOnLinkClickListener.onTelLinkClick(phoneNumber); } else if (schemeUrl.startsWith("mailto:")) { String mailAddr = Uri.parse(url).getSchemeSpecificPart(); mOnLinkClickListener.onMailLinkClick(mailAddr); } else if (schemeUrl.startsWith("http") || schemeUrl.startsWith("https")) { mOnLinkClickListener.onWebUrlLinkClick(url); } } } } }; public interface OnLinkClickListener { /** * 电话号码被点击 */ void onTelLinkClick(String phoneNumber); /** * 邮箱地址被点击 */ void onMailLinkClick(String mailAddress); /** * URL 被点击 */ void onWebUrlLinkClick(String url); } public interface OnLinkLongClickListener { void onLongClick(String text); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/textview/QMUISpanTouchFixTextView.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.widget.textview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.text.Spannable; import android.text.method.MovementMethod; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.appcompat.widget.AppCompatTextView; import com.qmuiteam.qmui.layout.IQMUILayout; import com.qmuiteam.qmui.layout.QMUILayoutHelper; import com.qmuiteam.qmui.link.QMUILinkTouchMovementMethod; import com.qmuiteam.qmui.span.QMUITouchableSpan; /** *

* 修复了 {@link TextView} 与 {@link android.text.style.ClickableSpan} 一起使用时, * 点击 {@link android.text.style.ClickableSpan} 也会触发 {@link TextView} 的事件的问题。 *

*

* 同时通过 {@link #setNeedForceEventToParent(boolean)} 控制该 TextView 的点击事件能否传递给其 Parent, * 修复了 {@link TextView} 默认情况下如果添加了 {@link android.text.style.ClickableSpan} 之后就无法把点击事件传递给 {@link TextView} 的 Parent 的问题。 *

*

* 注意: 使用该 {@link TextView} 时, 用 {@link QMUITouchableSpan} 代替 {@link android.text.style.ClickableSpan}, * 且同时可以使用 {@link QMUITouchableSpan} 达到修改 span 的文字颜色和背景色的目的。 *

*

* 注意: 使用该 {@link TextView} 时, 需调用 {@link #setMovementMethodDefault()} 方法设置默认的 {@link QMUILinkTouchMovementMethod}, * TextView 会在 {@link #onTouchEvent(MotionEvent)} 时将事件传递给 {@link QMUILinkTouchMovementMethod}, * 然后传递给 {@link QMUITouchableSpan}, 实现点击态的变化和点击事件的响应。 *

* * @author cginechen * @date 2017-03-20 * @see QMUITouchableSpan * @see QMUILinkTouchMovementMethod */ public class QMUISpanTouchFixTextView extends AppCompatTextView implements ISpanTouchFix, IQMUILayout { /** * 记录当前 Touch 事件对应的点是不是点在了 span 上面 */ private boolean mTouchSpanHit; /** * 记录每次真正传入的press,每次更改mTouchSpanHint,需要再调用一次setPressed,确保press状态正确 */ private boolean mIsPressedRecord = false; /** * TextView是否应该消耗事件 */ private boolean mNeedForceEventToParent = false; private QMUILayoutHelper mLayoutHelper; public QMUISpanTouchFixTextView(Context context) { this(context, null); } public QMUISpanTouchFixTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public QMUISpanTouchFixTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setHighlightColor(Color.TRANSPARENT); mLayoutHelper = new QMUILayoutHelper(context, attrs, defStyleAttr, this); } public void setNeedForceEventToParent(boolean needForceEventToParent) { mNeedForceEventToParent = needForceEventToParent; setFocusable(!needForceEventToParent); setClickable(!needForceEventToParent); setLongClickable(!needForceEventToParent); } /** * 使用者主动调用 */ public void setMovementMethodDefault(){ setMovementMethodCompat(QMUILinkTouchMovementMethod.getInstance()); } public void setMovementMethodCompat(MovementMethod movement){ setMovementMethod(movement); if(mNeedForceEventToParent){ setNeedForceEventToParent(true); } } @Override public boolean onTouchEvent(MotionEvent event) { if (!(getText() instanceof Spannable) || !(getMovementMethod() instanceof QMUILinkTouchMovementMethod)) { mTouchSpanHit = false; return super.onTouchEvent(event); } mTouchSpanHit = true; // 调用super.onTouchEvent,会走到QMUILinkTouchMovementMethod // 会走到QMUILinkTouchMovementMethod#onTouchEvent会修改mTouchSpanHint boolean ret = super.onTouchEvent(event); if(mNeedForceEventToParent){ return mTouchSpanHit; } return ret; } @Override public void setTouchSpanHit(boolean hit) { if (mTouchSpanHit != hit) { mTouchSpanHit = hit; setPressed(mIsPressedRecord); } } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean performClick() { if (!mTouchSpanHit && !mNeedForceEventToParent) { return super.performClick(); } return false; } @SuppressWarnings("SimplifiableIfStatement") @Override public boolean performLongClick() { if (!mTouchSpanHit && !mNeedForceEventToParent) { return super.performLongClick(); } return false; } @Override public final void setPressed(boolean pressed) { mIsPressedRecord = pressed; if (!mTouchSpanHit) { onSetPressed(pressed); } } protected void onSetPressed(boolean pressed) { super.setPressed(pressed); } @Override public void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.updateTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void updateBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.updateBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void updateLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.updateLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } public void updateRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.updateRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor) { mLayoutHelper.onlyShowTopDivider(topInsetLeft, topInsetRight, topDividerHeight, topDividerColor); invalidate(); } @Override public void onlyShowBottomDivider(int bottomInsetLeft, int bottomInsetRight, int bottomDividerHeight, int bottomDividerColor) { mLayoutHelper.onlyShowBottomDivider(bottomInsetLeft, bottomInsetRight, bottomDividerHeight, bottomDividerColor); invalidate(); } @Override public void onlyShowLeftDivider(int leftInsetTop, int leftInsetBottom, int leftDividerWidth, int leftDividerColor) { mLayoutHelper.onlyShowLeftDivider(leftInsetTop, leftInsetBottom, leftDividerWidth, leftDividerColor); invalidate(); } @Override public void onlyShowRightDivider(int rightInsetTop, int rightInsetBottom, int rightDividerWidth, int rightDividerColor) { mLayoutHelper.onlyShowRightDivider(rightInsetTop, rightInsetBottom, rightDividerWidth, rightDividerColor); invalidate(); } @Override public void setTopDividerAlpha(int dividerAlpha) { mLayoutHelper.setTopDividerAlpha(dividerAlpha); invalidate(); } @Override public void setBottomDividerAlpha(int dividerAlpha) { mLayoutHelper.setBottomDividerAlpha(dividerAlpha); invalidate(); } @Override public void setLeftDividerAlpha(int dividerAlpha) { mLayoutHelper.setLeftDividerAlpha(dividerAlpha); invalidate(); } @Override public void setRightDividerAlpha(int dividerAlpha) { mLayoutHelper.setRightDividerAlpha(dividerAlpha); invalidate(); } @Override public void setHideRadiusSide(int hideRadiusSide) { mLayoutHelper.setHideRadiusSide(hideRadiusSide); invalidate(); } @Override public int getHideRadiusSide() { return mLayoutHelper.getHideRadiusSide(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { widthMeasureSpec = mLayoutHelper.getMeasuredWidthSpec(widthMeasureSpec); heightMeasureSpec = mLayoutHelper.getMeasuredHeightSpec(heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int minW = mLayoutHelper.handleMiniWidth(widthMeasureSpec, getMeasuredWidth()); int minH = mLayoutHelper.handleMiniHeight(heightMeasureSpec, getMeasuredHeight()); if (widthMeasureSpec != minW || heightMeasureSpec != minH) { super.onMeasure(minW, minH); } } @Override public void setRadiusAndShadow(int radius, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide, int shadowElevation, final float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowAlpha); } @Override public void setRadiusAndShadow(int radius, int hideRadiusSide, int shadowElevation, int shadowColor, float shadowAlpha) { mLayoutHelper.setRadiusAndShadow(radius, hideRadiusSide, shadowElevation, shadowColor, shadowAlpha); } @Override public void setRadius(int radius) { mLayoutHelper.setRadius(radius); } @Override public void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide) { mLayoutHelper.setRadius(radius, hideRadiusSide); } @Override public int getRadius() { return mLayoutHelper.getRadius(); } @Override public void setOutlineInset(int left, int top, int right, int bottom) { mLayoutHelper.setOutlineInset(left, top, right, bottom); } @Override public void setBorderColor(@ColorInt int borderColor) { mLayoutHelper.setBorderColor(borderColor); invalidate(); } @Override public void setBorderWidth(int borderWidth) { mLayoutHelper.setBorderWidth(borderWidth); invalidate(); } @Override public void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL) { mLayoutHelper.setShowBorderOnlyBeforeL(showBorderOnlyBeforeL); invalidate(); } @Override public boolean setWidthLimit(int widthLimit) { if (mLayoutHelper.setWidthLimit(widthLimit)) { requestLayout(); invalidate(); } return true; } @Override public boolean setHeightLimit(int heightLimit) { if (mLayoutHelper.setHeightLimit(heightLimit)) { requestLayout(); invalidate(); } return true; } @Override public void setUseThemeGeneralShadowElevation() { mLayoutHelper.setUseThemeGeneralShadowElevation(); } @Override public void setOutlineExcludePadding(boolean outlineExcludePadding) { mLayoutHelper.setOutlineExcludePadding(outlineExcludePadding); } @Override public void setShadowElevation(int elevation) { mLayoutHelper.setShadowElevation(elevation); } @Override public int getShadowElevation() { return mLayoutHelper.getShadowElevation(); } @Override public void setShadowAlpha(float shadowAlpha) { mLayoutHelper.setShadowAlpha(shadowAlpha); } @Override public float getShadowAlpha() { return mLayoutHelper.getShadowAlpha(); } @Override public void setShadowColor(int shadowColor) { mLayoutHelper.setShadowColor(shadowColor); } @Override public int getShadowColor() { return mLayoutHelper.getShadowColor(); } @Override public void setOuterNormalColor(int color) { mLayoutHelper.setOuterNormalColor(color); } @Override public void updateBottomSeparatorColor(int color) { mLayoutHelper.updateBottomSeparatorColor(color); } @Override public void updateLeftSeparatorColor(int color) { mLayoutHelper.updateLeftSeparatorColor(color); } @Override public void updateRightSeparatorColor(int color) { mLayoutHelper.updateRightSeparatorColor(color); } @Override public void updateTopSeparatorColor(int color) { mLayoutHelper.updateTopSeparatorColor(color); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mLayoutHelper.drawDividers(canvas, getWidth(), getHeight()); mLayoutHelper.dispatchRoundBorderDraw(canvas); } @Override public boolean hasBorder() { return mLayoutHelper.hasBorder(); } @Override public boolean hasLeftSeparator() { return mLayoutHelper.hasLeftSeparator(); } @Override public boolean hasTopSeparator() { return mLayoutHelper.hasTopSeparator(); } @Override public boolean hasRightSeparator() { return mLayoutHelper.hasRightSeparator(); } @Override public boolean hasBottomSeparator() { return mLayoutHelper.hasBottomSeparator(); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIBridgeWebViewClient.java ================================================ package com.qmuiteam.qmui.widget.webview; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.webkit.WebResourceRequest; import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.util.QMUILangHelper; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; public class QMUIBridgeWebViewClient extends QMUIWebViewClient { public static final String QMUI_BRIDGE_HAS_MESSAGE = "qmui://__QUEUE_MESSAGE__"; public static final String QMUI_BRIDGE_JS = "QMUIWebviewBridge.js"; private QMUIWebViewBridgeHandler mWebViewBridgeHandler; private boolean mNeedInjectLocalBridgeJs; public QMUIBridgeWebViewClient(boolean needDispatchSafeAreaInset, boolean disableVideoFullscreenBtnAlways, @NonNull QMUIWebViewBridgeHandler bridgeHandler) { this(needDispatchSafeAreaInset, disableVideoFullscreenBtnAlways, true, bridgeHandler); } public QMUIBridgeWebViewClient(boolean needDispatchSafeAreaInset, boolean disableVideoFullscreenBtnAlways, boolean needInjectLocalBridgeJs, @NonNull QMUIWebViewBridgeHandler bridgeHandler) { super(needDispatchSafeAreaInset, disableVideoFullscreenBtnAlways); mNeedInjectLocalBridgeJs = needInjectLocalBridgeJs; mWebViewBridgeHandler = bridgeHandler; } @Override public final boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(QMUI_BRIDGE_HAS_MESSAGE)) { mWebViewBridgeHandler.fetchAndMessageFromJs(); return true; } return onShouldOverrideUrlLoading(view, url); } protected boolean onShouldOverrideUrlLoading(WebView view, String url){ return super.shouldOverrideUrlLoading(view, url); } @Override public final boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { String url = request.getUrl().toString(); if (url.startsWith(QMUI_BRIDGE_HAS_MESSAGE)) { mWebViewBridgeHandler.fetchAndMessageFromJs(); return true; } } return onShouldOverrideUrlLoading(view, request); } @TargetApi(24) protected boolean onShouldOverrideUrlLoading(WebView view, WebResourceRequest request){ return super.shouldOverrideUrlLoading(view, request); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if(mNeedInjectLocalBridgeJs){ String bridgeScript = loadBridgeScript(view.getContext()); if (bridgeScript != null) { view.evaluateJavascript(bridgeScript, null); mWebViewBridgeHandler.onBridgeLoaded(); } }else{ mWebViewBridgeHandler.onBridgeLoaded(); } } @Nullable private static String loadBridgeScript(Context context) { InputStream in = null; try { in = context.getAssets().open(QMUI_BRIDGE_JS); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); String line = bufferedReader.readLine(); StringBuilder sb = new StringBuilder(); while (line != null) { sb.append(line); sb.append("\n"); line = bufferedReader.readLine(); } bufferedReader.close(); in.close(); return sb.toString(); } catch (Exception e) { e.printStackTrace(); } finally { QMUILangHelper.close(in); } return null; } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebView.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.widget.webview; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class QMUIWebView extends WebView { private static final String TAG = "QMUIWebView"; private static boolean sIsReflectionOccurError = false; private Object mAwContents; private Object mWebContents; private Method mSetDisplayCutoutSafeAreaMethod; private Rect mSafeAreaRectCache; /** * if true, the web content may be located under status bar */ private boolean mNeedDispatchSafeAreaInset = false; private Callback mCallback; private List mOnScrollChangeListeners = new ArrayList<>(); public QMUIWebView(Context context) { super(context); init(); } public QMUIWebView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public QMUIWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { removeJavascriptInterface("searchBoxJavaBridge_"); removeJavascriptInterface("accessibility"); removeJavascriptInterface("accessibilityTraversal"); QMUIWindowInsetHelper.handleWindowInsets(this, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout(), new QMUIWindowInsetHelper.InsetHandler() { @Override public void handleInset(View view, Insets insets) { if (mNeedDispatchSafeAreaInset) { float density = QMUIDisplayHelper.getDensity(getContext()); Rect rect = new Rect( (int) (insets.left / density + getExtraInsetLeft(density)), (int) (insets.top / density + getExtraInsetTop(density)), (int) (insets.right / density + getExtraInsetRight(density)), (int) (insets.bottom / density + getExtraInsetBottom(density)) ); setStyleDisplayCutoutSafeArea(rect); } } }, true, false, false); } @Override public void addJavascriptInterface(Object object, String name) { } @Deprecated public void setCustomOnScrollChangeListener(OnScrollChangeListener onScrollChangeListener) { addCustomOnScrollChangeListener(onScrollChangeListener); } public void addCustomOnScrollChangeListener(OnScrollChangeListener listener) { if (!mOnScrollChangeListeners.contains(listener)) { mOnScrollChangeListeners.add(listener); } } public void removeOnScrollChangeListener(OnScrollChangeListener listener) { mOnScrollChangeListeners.remove(listener); } public void removeAllOnScrollChangeListener(){ mOnScrollChangeListeners.clear(); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); for (OnScrollChangeListener onScrollListener : mOnScrollChangeListeners) { onScrollListener.onScrollChange(this, l, t, oldl, oldt); } } @Override public void setWebViewClient(WebViewClient client) { if (client != null && !(client instanceof QMUIWebViewClient)) { throw new IllegalArgumentException("must use the instance of QMUIWebViewClient"); } super.setWebViewClient(client); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return super.dispatchKeyEvent(event); } public void setNeedDispatchSafeAreaInset(boolean needDispatchSafeAreaInset) { if (mNeedDispatchSafeAreaInset != needDispatchSafeAreaInset) { mNeedDispatchSafeAreaInset = needDispatchSafeAreaInset; if (ViewCompat.isAttachedToWindow(this)) { if (needDispatchSafeAreaInset) { ViewCompat.requestApplyInsets(this); } else { // clear insets setStyleDisplayCutoutSafeArea(new Rect()); } } } } public boolean isNeedDispatchSafeAreaInset() { return mNeedDispatchSafeAreaInset; } public void setCallback(Callback callback) { mCallback = callback; } private void doNotSupportChangeCssEnv() { sIsReflectionOccurError = true; if (mCallback != null) { mCallback.onSureNotSupportChangeCssEnv(); } } boolean isNotSupportChangeCssEnv() { return sIsReflectionOccurError; } protected int getExtraInsetTop(float density) { return 0; } protected int getExtraInsetLeft(float density) { return 0; } protected int getExtraInsetRight(float density) { return 0; } protected int getExtraInsetBottom(float density) { return 0; } @Override public void destroy() { mAwContents = null; mWebContents = null; mSetDisplayCutoutSafeAreaMethod = null; stopLoading(); super.destroy(); } private void setStyleDisplayCutoutSafeArea(@NonNull Rect rect) { if (sIsReflectionOccurError || Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { return; } if (rect == mSafeAreaRectCache) { return; } if (mSafeAreaRectCache == null) { mSafeAreaRectCache = new Rect(rect); } else { mSafeAreaRectCache.set(rect); } long start = System.currentTimeMillis(); if (mAwContents == null || mWebContents == null || mSetDisplayCutoutSafeAreaMethod == null) { try { Field providerField = WebView.class.getDeclaredField("mProvider"); providerField.setAccessible(true); Object provider = providerField.get(this); mAwContents = getAwContentsFieldValueInProvider(provider); if (mAwContents == null) { return; } mWebContents = getWebContentsFieldValueInAwContents(mAwContents); if (mWebContents == null) { return; } mSetDisplayCutoutSafeAreaMethod = getSetDisplayCutoutSafeAreaMethodInWebContents(mWebContents); if (mSetDisplayCutoutSafeAreaMethod == null) { // no such method, maybe the old version doNotSupportChangeCssEnv(); return; } } catch (Exception e) { doNotSupportChangeCssEnv(); Log.i(TAG, "setStyleDisplayCutoutSafeArea error: " + e); } } try { mSetDisplayCutoutSafeAreaMethod.setAccessible(true); mSetDisplayCutoutSafeAreaMethod.invoke(mWebContents, rect); } catch (Exception e) { sIsReflectionOccurError = true; Log.i(TAG, "setStyleDisplayCutoutSafeArea error: " + e); } Log.i(TAG, "setDisplayCutoutSafeArea speed time: " + (System.currentTimeMillis() - start)); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); ViewCompat.requestApplyInsets(this); } private Object getAwContentsFieldValueInProvider(Object provider) throws IllegalAccessException, NoSuchFieldException { try { Field awContentsField = provider.getClass().getDeclaredField("mAwContents"); if (awContentsField != null) { awContentsField.setAccessible(true); return awContentsField.get(provider); } } catch (NoSuchFieldException ignored) { } // Unfortunately, the source code is ugly in some roms, so we can not reflect the field/method by name for (Field field : provider.getClass().getDeclaredFields()) { // 1. get field mAwContents field.setAccessible(true); Object awContents = field.get(provider); if (awContents == null) { continue; } if (awContents.getClass().getSimpleName().equals("AwContents")) { return awContents; } } return null; } private Object getWebContentsFieldValueInAwContents(Object awContents) throws IllegalAccessException { try { Field webContentsField = awContents.getClass().getDeclaredField("mWebContents"); if (webContentsField != null) { webContentsField.setAccessible(true); return webContentsField.get(awContents); } } catch (NoSuchFieldException ignored) { } // Unfortunately, the source code is ugly in some roms, so we can not reflect the field/method by name for (Field innerField : awContents.getClass().getDeclaredFields()) { innerField.setAccessible(true); Object webContents = innerField.get(awContents); if (webContents == null) { continue; } if (webContents.getClass().getSimpleName().equals("WebContentsImpl")) { return webContents; } } return null; } private Method getSetDisplayCutoutSafeAreaMethodInWebContents(Object webContents) { try { return webContents.getClass() .getDeclaredMethod("setDisplayCutoutSafeArea", Rect.class); } catch (NoSuchMethodException ignored) { } // Unfortunately, the source code is ugly in some roms, so we can not reflect the field/method by name // not very safe in future for (Method method : webContents.getClass().getDeclaredMethods()) { if (method.getReturnType() == void.class && method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == Rect.class) { return method; } } return null; } public interface Callback { void onSureNotSupportChangeCssEnv(); } public interface OnScrollChangeListener { /** * Called when the scroll position of a view changes. * * @param webView The view whose scroll position has changed. * @param scrollX Current horizontal scroll origin. * @param scrollY Current vertical scroll origin. * @param oldScrollX Previous horizontal scroll origin. * @param oldScrollY Previous vertical scroll origin. */ void onScrollChange(WebView webView, int scrollX, int scrollY, int oldScrollX, int oldScrollY); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewBridgeHandler.java ================================================ package com.qmuiteam.qmui.widget.webview; import android.util.Pair; import android.webkit.ValueCallback; import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; public abstract class QMUIWebViewBridgeHandler { private static final String MESSAGE_JS_FETCH_SCRIPT = "QMUIBridge._fetchQueueFromNative()"; private static final String MESSAGE_JS_RESPONSE_SCRIPT = "QMUIBridge._handleResponseFromNative($data$)"; private static final String MESSAGE_PARAM_HOLDER = "$data$"; private static final String MESSAGE_CALLBACK_ID = "callbackId"; private static final String MESSAGE_DATA = "data"; private static final String MESSAGE_INNER_CMD_NAME = "__cmd__"; private static final String MESSAGE_CMD_GET_SUPPORTED_CMD_LIST = "getSupportedCmdList"; private List>> mStartupMessageList = new ArrayList<>(); private WebView mWebView; public QMUIWebViewBridgeHandler(@NonNull WebView webView) { mWebView = webView; } public final void evaluateBridgeScript(String script, ValueCallback resultCallback) { if (mStartupMessageList != null) { mStartupMessageList.add(new Pair<>(script, resultCallback)); } else { mWebView.evaluateJavascript(script, resultCallback); } } void onBridgeLoaded() { if (mStartupMessageList != null) { for (Pair> message : mStartupMessageList) { mWebView.evaluateJavascript(message.first, message.second); } mStartupMessageList = null; } } void fetchAndMessageFromJs() { mWebView.evaluateJavascript(MESSAGE_JS_FETCH_SCRIPT, new ValueCallback() { @Override public void onReceiveValue(String value) { String unescaped = unescape(value); if (unescaped != null) { try { JSONArray array = new JSONArray(unescaped); for (int i = 0; i < array.length(); i++) { JSONObject message = array.getJSONObject(i); String callbackId = message.getString(MESSAGE_CALLBACK_ID); String msgDataOrigin = message.getString(MESSAGE_DATA); MessageFinishCallback callback = new MessageFinishCallback(callbackId) { @Override public void finish(Object data) { try{ JSONObject response = new JSONObject(); response.put(MESSAGE_CALLBACK_ID, getCallbackId()); response.put(MESSAGE_DATA, data); String script = MESSAGE_JS_RESPONSE_SCRIPT.replace(MESSAGE_PARAM_HOLDER, response.toString()); mWebView.evaluateJavascript(script, null); }catch (Throwable ignore){ } } }; try{ JSONObject msgData = new JSONObject(msgDataOrigin); String cmdName = msgData.getString(MESSAGE_INNER_CMD_NAME); handleInnerMessage(cmdName, msgData, callback); }catch (Throwable e){ handleMessage(msgDataOrigin, callback); } } } catch (JSONException e) { e.printStackTrace(); } } } }); } private void handleInnerMessage(String cmdName, JSONObject jsonObject, MessageFinishCallback callback){ if(MESSAGE_CMD_GET_SUPPORTED_CMD_LIST.equals(cmdName)){ callback.finish(new JSONArray(getSupportedCmdList())); }else{ throw new RuntimeException("not a inner api message. fallback to custom message"); } } protected abstract List getSupportedCmdList(); protected abstract void handleMessage(String message, MessageFinishCallback callback); @Nullable public static String unescape(@Nullable String value) { if (value == null || value.isEmpty()) { return null; } String ret = value.substring(1, value.length() - 1) .replace("\\\\", "\\") .replace("\\\"", "\""); if ("null".equals(ret)) { return null; } return ret; } @NonNull public static String escape(@Nullable String value) { if (value == null || value.isEmpty()) { return "\"null\""; } String ret = value .replace("\\", "\\\\") .replace("\"", "\\\""); return "\"" + ret + "\""; } public abstract class MessageFinishCallback{ private final String mCallbackId; public MessageFinishCallback(String callbackId){ mCallbackId = callbackId; } public String getCallbackId() { return mCallbackId; } public abstract void finish(Object data); } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewClient.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.widget.webview; import android.graphics.Bitmap; import android.os.SystemClock; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.webkit.ValueCallback; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class QMUIWebViewClient extends WebViewClient { public static final int JS_FAKE_KEY_CODE_EVENT = 112; // F1 private boolean mNeedDispatchSafeAreaInset; private boolean mDisableVideoFullscreenBtnAlways; private boolean mIsPageFinished = false; public QMUIWebViewClient(boolean needDispatchSafeAreaInset, boolean disableVideoFullscreenBtnAlways) { mNeedDispatchSafeAreaInset = needDispatchSafeAreaInset; mDisableVideoFullscreenBtnAlways = disableVideoFullscreenBtnAlways; } public void setNeedDispatchSafeAreaInset(QMUIWebView webView) { if (!mNeedDispatchSafeAreaInset) { mNeedDispatchSafeAreaInset = true; if (mIsPageFinished) { dispatchFullscreenRequestAction(webView); } } } @Override public void onPageStarted(WebView view, String url, @Nullable Bitmap favicon) { mIsPageFinished = false; super.onPageStarted(view, url, favicon); } @Override public void onPageFinished(final WebView view, String url) { super.onPageFinished(view, url); mIsPageFinished = true; if (mDisableVideoFullscreenBtnAlways) { runJsCode(view, getJsCodeForDisableVideoFullscreenBtn(), null); } if (mNeedDispatchSafeAreaInset && view instanceof QMUIWebView) { dispatchFullscreenRequestAction((QMUIWebView) view); } } private String getJsCodeForDisableVideoFullscreenBtn() { return "(function(){\n" + // disable fullscreen btn on video " var head = document.getElementsByTagName('head')[0];\n" + " var style = document.createElement('style');\n" + " style.type = 'text/css';" + " style.innerHTML = 'video::-webkit-media-controls-fullscreen-button{display: none !important;}'\n" + " head.appendChild(style);\n" + "})()"; } private String getJsCodeForFullscreenHtml() { return "(function(){\n" + " document.body.addEventListener('keydown', function(e){\n" + " if(e.keyCode == " + JS_FAKE_KEY_CODE_EVENT + "){\n" + " var html = document.documentElement;\n" + " var requestFullscreen = html.requestFullscreen || html.webkitRequestFullscreen;\n" + " requestFullscreen.call(html);\n" + " }\n" + " })\n" + "})()"; } private void dispatchFullscreenRequestAction(final QMUIWebView webView) { boolean sureNotSupportModifyCssEnv = webView.isNotSupportChangeCssEnv(); if (sureNotSupportModifyCssEnv) { return; } if (!mDisableVideoFullscreenBtnAlways) { runJsCode(webView, getJsCodeForDisableVideoFullscreenBtn(), null); } runJsCode(webView, getJsCodeForFullscreenHtml(), new Runnable() { @Override public void run() { dispatchFullscreenRequestEvent(webView); } }); } private void dispatchFullscreenRequestEvent(WebView webView) { KeyEvent keyEvent = new KeyEvent(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_F1, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0); webView.dispatchKeyEvent(keyEvent); } private void runJsCode(WebView webView, @NonNull String jsCode, @Nullable final Runnable finishAction) { if (finishAction == null) { webView.evaluateJavascript(jsCode, null); } else { webView.evaluateJavascript(jsCode, new ValueCallback() { @Override public void onReceiveValue(String value) { finishAction.run(); } }); } } } ================================================ FILE: qmui/src/main/java/com/qmuiteam/qmui/widget/webview/QMUIWebViewContainer.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.widget.webview; import android.content.Context; import android.util.AttributeSet; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; public class QMUIWebViewContainer extends QMUIFrameLayout { private QMUIWebView mWebView; private QMUIWebView.OnScrollChangeListener mOnScrollChangeListener; public QMUIWebViewContainer(Context context) { super(context); } public QMUIWebViewContainer(Context context, AttributeSet attrs) { super(context, attrs); } public void addWebView(@NonNull QMUIWebView webView, boolean needDispatchSafeAreaInset) { mWebView = webView; mWebView.setNeedDispatchSafeAreaInset(needDispatchSafeAreaInset); mWebView.addCustomOnScrollChangeListener(new QMUIWebView.OnScrollChangeListener() { @Override public void onScrollChange(WebView webView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { if (mOnScrollChangeListener != null) { mOnScrollChangeListener.onScrollChange(webView, scrollX, scrollY, oldScrollX, oldScrollY); } } }); addView(mWebView, getWebViewLayoutParams()); QMUIWindowInsetHelper.handleWindowInsets(this, WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); } protected FrameLayout.LayoutParams getWebViewLayoutParams() { return new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } public void setNeedDispatchSafeAreaInset(boolean needDispatchSafeAreaInset) { if (mWebView != null) { mWebView.setNeedDispatchSafeAreaInset(needDispatchSafeAreaInset); } } public void destroy() { removeView(mWebView); removeAllViews(); mWebView.setWebChromeClient(null); mWebView.setWebViewClient(null); mWebView.destroy(); } public void setCustomOnScrollChangeListener(QMUIWebView.OnScrollChangeListener onScrollChangeListener) { mOnScrollChangeListener = onScrollChangeListener; } } ================================================ FILE: qmui/src/main/res/anim/decelerate_factor_interpolator.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/decelerate_low_factor_interpolator.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_bottom.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_bottomleft_to_topright.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_bottomright_to_topleft.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_top.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_topleft_to_bottomright.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/grow_from_topright_to_bottomleft.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/scale_in_center.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/scale_out_center.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_bottom.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_bottomleft_to_topright.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_bottomright_to_topleft.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_top.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_topleft_to_bottomright.xml ================================================ ================================================ FILE: qmui/src/main/res/anim/shrink_from_topright_to_bottomleft.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_btn_blue_bg.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_btn_blue_border.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_btn_blue_text.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_s_link_color.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_s_list_item_text_color.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_s_switch_text_color.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_s_transparent.xml ================================================ ================================================ FILE: qmui/src/main/res/color/qmui_topbar_text_color.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_divider_bottom_bitmap.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_divider_top_bitmap.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_popup_close.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_popup_close_with_bg.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_pull_down.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_quick_action_more_arrow_left.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_quick_action_more_arrow_right.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_icon_topbar_back.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_checkbox.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_icon_switch.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_list_item_bg_1.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_list_item_bg_2.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_switch_thumb.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_s_switch_track.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_switch_thumb.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_switch_thumb_checked.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_switch_track.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_switch_track_checked.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable/qmui_tips_point.xml ================================================ ================================================ FILE: qmui/src/main/res/drawable-v21/qmui_s_list_item_bg_2.xml ================================================ ================================================ FILE: qmui/src/main/res/layout/qmui_bottom_sheet_dialog.xml ================================================ ================================================ FILE: qmui/src/main/res/layout/qmui_common_list_item.xml ================================================ ================================================ FILE: qmui/src/main/res/layout/qmui_empty_view.xml ================================================ ================================================ FILE: qmui/src/main/res/layout/qmui_group_list_section_layout.xml ================================================ ================================================ FILE: qmui/src/main/res/values/config_colors.xml ================================================ @color/qmui_config_color_blue #801B88EE #801B88EE @color/qmui_config_color_blue #801B88EE #801B88EE #416f96 #243f55 #80416f96 ================================================ FILE: qmui/src/main/res/values/qmui_attrs.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_attrs_alpha.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_attrs_base.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_attrs_custom.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_attrs_layout.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_attrs_round.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_colors.xml ================================================ #00000000 #ffffff #C0FFFFFF #80FFFFFF #40FFFFFF #26FFFFFF #19FFFFFF #000000 #C0000000 #99000000 #80000000 #40000000 #26000000 #19000000 #1B88EE #801B88EE #FA3A3A #DEE0E2 #D4D6D8 #F4F5F7 #EEEFF1 #212832 #547FB0 #EEEEF0 #353C46 #49505A #5D646E #717882 #858C96 #99A0AA #ADB4BE #C4C8D0 #D8DCE4 #DEE0E2 #DEE0E2 ================================================ FILE: qmui/src/main/res/values/qmui_dimens.xml ================================================ 2dp 16sp 1dp 20dp 16dp 1px -1px 13sp 2dp 16sp 14dp 6dp 56dp 103dp 8dp 13sp 800dp 120dp @dimen/qmui_content_spacing_horizontal 16dp 8dp ================================================ FILE: qmui/src/main/res/values/qmui_ids.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_strings.xml ================================================ \u200b QMUI 取 消 ================================================ FILE: qmui/src/main/res/values/qmui_style_appearance.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_style_widget.xml ================================================ ================================================ FILE: qmui/src/main/res/values/qmui_themes.xml ================================================ ================================================ FILE: qmui/src/main/res/values-v21/qmui_themes.xml ================================================ ================================================ FILE: qmuidemo/.gitignore ================================================ .idea .DS_Store local.properties /*.iml /build release.properties qmuidemo.keystore ================================================ FILE: qmuidemo/build.gradle.kts ================================================ import com.qmuiteam.plugin.Dep import java.io.ByteArrayOutputStream import java.util.* plugins { id("com.android.application") kotlin("android") kotlin("kapt") } fun runCommand(project: Project, command: String): String { val stdout = ByteArrayOutputStream() project.exec { commandLine = command.split(" ") standardOutput = stdout } return stdout.toString().trim() } val gitVersion = runCommand(project, "git rev-list HEAD --count").toIntOrNull() ?: 1 android { signingConfigs { val properties = Properties() val propFile = project.file("release.properties") if (propFile.exists()) { properties.load(propFile.inputStream()) } create("release"){ keyAlias = properties.getProperty("RELEASE_KEY_ALIAS") keyPassword = properties.getProperty("RELEASE_KEY_PASSWORD") storeFile = file("qmuidemo.keystore") storePassword = properties.getProperty("RELEASE_STORE_PASSWORD") enableV2Signing = true } } compileSdk = Dep.compileSdk compileOptions { sourceCompatibility = Dep.javaVersion targetCompatibility = Dep.javaVersion } kotlinOptions { jvmTarget = Dep.kotlinJvmTarget freeCompilerArgs += "-Xjvm-default=all" } buildFeatures { compose = true buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = Dep.Compose.version } defaultConfig { applicationId = "com.qmuiteam.qmuidemo" minSdk = Dep.minSdk targetSdk = Dep.targetSdk versionCode = gitVersion versionName = Dep.QMUI.qmuiVer ndk { abiFilters.add("arm64-v8a") } } buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.getByName("release") } } } dependencies { implementation(Dep.AndroidX.appcompat) implementation(Dep.AndroidX.annotation) implementation(Dep.AndroidX.activity) implementation(Dep.MaterialDesign.material) implementation(Dep.ButterKnife.butterknife) implementation(Dep.Compose.activity) implementation(Dep.Compose.constraintlayout) kapt(Dep.ButterKnife.compiler) implementation(project(":lib")) implementation(project(":qmui")) implementation(project(":arch")) implementation(project(":type")) implementation(project(":compose")) implementation(project(":photo")) implementation(project(":photo-coil")) implementation(project(":photo-glide")) implementation(project(":editor")) implementation(Dep.Flipper.soLoader) implementation(Dep.Flipper.flipper) kapt(project(":compiler")) kapt(project(":arch-compiler")) kapt(Dep.Glide.compiler) implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0") } ================================================ FILE: qmuidemo/lint.xml ================================================ ================================================ FILE: qmuidemo/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/chant/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # 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 *; #} -keep class **_FragmentFinder { *; } -keep class androidx.fragment.app.* { *; } -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMap { *; } -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMapImpl { *; } -keep class com.qmuiteam.qmui.arch.scheme.SchemeMap {*;} -keep class com.qmuiteam.qmui.arch.scheme.SchemeMapImpl {*;} -keep class com.facebook.jni.**{*;} -keep class com.facebook.flipper.**{*;} ================================================ FILE: qmuidemo/src/main/AndroidManifest.xml ================================================ ================================================ FILE: qmuidemo/src/main/assets/demo.html ================================================ js调用java
================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDApplication.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.qmuidemo import android.annotation.SuppressLint import android.app.Application import android.content.ContentValues import android.content.Context import android.content.res.Configuration import android.os.Environment import android.provider.MediaStore import android.util.Log import coil.ImageLoader import coil.ImageLoaderFactory import com.facebook.flipper.android.AndroidFlipperClient import com.facebook.flipper.plugins.inspector.DescriptorMapping import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin import com.facebook.soloader.SoLoader import com.qmuiteam.qmui.QMUILog import com.qmuiteam.qmui.QMUILog.QMUILogDelegate import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager import com.qmuiteam.qmui.qqface.QMUIQQFaceCompiler import com.qmuiteam.qmuidemo.manager.QDSkinManager import com.qmuiteam.qmuidemo.manager.QDUpgradeManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import xcrash.TombstoneManager import xcrash.XCrash import java.io.File /** * Demo 的 Application 入口。 * Created by cgine on 16/3/22. */ class QDApplication : Application(), ImageLoaderFactory { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) XCrash.init(this) } override fun onCreate() { super.onCreate() context = applicationContext QMUILog.setDelegete(object : QMUILogDelegate { override fun e(tag: String, msg: String, vararg obj: Any) { Log.e(tag, msg) } override fun w(tag: String, msg: String, vararg obj: Any) { Log.w(tag, msg) } override fun i(tag: String, msg: String, vararg obj: Any) { Log.i(tag, msg) } override fun d(tag: String, msg: String, vararg obj: Any) { Log.d(tag, msg) } override fun printErrStackTrace(tag: String, tr: Throwable, format: String, vararg obj: Any) {} }) QDUpgradeManager.getInstance(this).check() QMUISwipeBackActivityManager.init(this) QMUIQQFaceCompiler.setDefaultQQFaceManager(QDQQFaceManager.getInstance()) QDSkinManager.install(this) if(BuildConfig.DEBUG){ SoLoader.init(this, false) val client = AndroidFlipperClient.getInstance(this) client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) client.start() } GlobalScope.launch(Dispatchers.IO) { delay(5000) for (file in TombstoneManager.getAllTombstones()) { try { val contentValues = ContentValues() contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, file.name) contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "txt") val uri = contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues) ?: continue contentResolver.openOutputStream(uri)?.use { out -> file.inputStream().use { ins -> ins.copyTo(out) } } file.delete() }catch (ignore: Throwable){ } } } } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) if (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { QDSkinManager.changeSkin(QDSkinManager.SKIN_DARK) } else if (QDSkinManager.getCurrentSkin() == QDSkinManager.SKIN_DARK) { QDSkinManager.changeSkin(QDSkinManager.SKIN_BLUE) } } override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(applicationContext) .crossfade(true) .build() } companion object { @JvmStatic @SuppressLint("StaticFieldLeak") var context: Context? = null private set } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDMainActivity.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; import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_TITLE; import static com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment.EXTRA_URL; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentContainerView; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.QMUIFragmentActivity; import com.qmuiteam.qmui.arch.annotation.DefaultFirstFragment; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIStatusBarHelper; import com.qmuiteam.qmui.util.QMUIViewOffsetHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView2; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.popup.QMUIPopup; import com.qmuiteam.qmui.widget.popup.QMUIPopups; import com.qmuiteam.qmuidemo.base.BaseFragmentActivity; import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment; import com.qmuiteam.qmuidemo.fragment.home.HomeFragment; import com.qmuiteam.qmuidemo.manager.QDSkinManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; @DefaultFirstFragment(HomeFragment.class) @LatestVisitRecord public class QDMainActivity extends BaseFragmentActivity { private QMUIPopup mGlobalAction; private QMUISkinManager.OnSkinChangeListener mOnSkinChangeListener = new QMUISkinManager.OnSkinChangeListener() { @Override public void onSkinChange(QMUISkinManager skinManager, int oldSkin, int newSkin) { if (newSkin == QDSkinManager.SKIN_WHITE) { QMUIStatusBarHelper.setStatusBarLightMode(QDMainActivity.this); } else { QMUIStatusBarHelper.setStatusBarDarkMode(QDMainActivity.this); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); QMUISkinManager skinManager = QMUISkinManager.defaultInstance(this); setSkinManager(skinManager); mOnSkinChangeListener.onSkinChange(skinManager, -1, skinManager.getCurrentSkin()); } @Override protected RootView onCreateRootView(int fragmentContainerId) { return new CustomRootView(this, fragmentContainerId); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override protected void onStart() { super.onStart(); if (getSkinManager() != null) { getSkinManager().addSkinChangeListener(mOnSkinChangeListener); } } @Override protected void onResume() { super.onResume(); } @Override protected void onStop() { super.onStop(); if (getSkinManager() != null) { getSkinManager().removeSkinChangeListener(mOnSkinChangeListener); } } private void showGlobalActionPopup(View v) { String[] listItems = new String[]{ "Change Skin" }; List data = new ArrayList<>(); Collections.addAll(data, listItems); ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.simple_list_item, data); AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { if (i == 0) { final String[] items = new String[]{"蓝色(默认)", "黑色", "白色"}; new QMUIDialog.MenuDialogBuilder(QDMainActivity.this) .addItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { QDSkinManager.changeSkin(which + 1); dialog.dismiss(); } }) .setSkinManager(QMUISkinManager.defaultInstance(QDMainActivity.this)) .create() .show(); } if (mGlobalAction != null) { mGlobalAction.dismiss(); } } }; mGlobalAction = QMUIPopups.listPopup(this, QMUIDisplayHelper.dp2px(this, 250), QMUIDisplayHelper.dp2px(this, 300), adapter, onItemClickListener) .animStyle(QMUIPopup.ANIM_GROW_FROM_CENTER) .preferredDirection(QMUIPopup.DIRECTION_TOP) .shadow(true) .edgeProtection(QMUIDisplayHelper.dp2px(this, 10)) .offsetYIfTop(QMUIDisplayHelper.dp2px(this, 5)) .skinManager(QMUISkinManager.defaultInstance(this)) .show(v); } public static Intent createWebExplorerIntent(Context context, String url, String title) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_URL, url); bundle.putString(EXTRA_TITLE, title); return of(context, QDWebExplorerFragment.class, bundle); } public static Intent of(@NonNull Context context, @NonNull Class firstFragment) { return QMUIFragmentActivity.intentOf(context, QDMainActivity.class, firstFragment); } public static Intent of(@NonNull Context context, @NonNull Class firstFragment, @Nullable Bundle fragmentArgs) { return QMUIFragmentActivity.intentOf(context, QDMainActivity.class, firstFragment, fragmentArgs); } class CustomRootView extends RootView { private FragmentContainerView fragmentContainer; private QMUIRadiusImageView2 globalBtn; private QMUIViewOffsetHelper globalBtnOffsetHelper; private int btnSize; private final int touchSlop; private float touchDownX = 0; private float touchDownY = 0; private float lastTouchX = 0; private float lastTouchY = 0; private boolean isDragging; private boolean isTouchDownInGlobalBtn = false; public CustomRootView(Context context, int fragmentContainerId) { super(context, fragmentContainerId); btnSize = QMUIDisplayHelper.dp2px(context, 56); fragmentContainer = new FragmentContainerView(context); fragmentContainer.setId(fragmentContainerId); addView(fragmentContainer, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); globalBtn = new QMUIRadiusImageView2(context); globalBtn.setImageResource(R.mipmap.icon_theme); globalBtn.setScaleType(ImageView.ScaleType.CENTER_INSIDE); globalBtn.setRadiusAndShadow(btnSize / 2, QMUIDisplayHelper.dp2px(getContext(), 16), 0.4f); globalBtn.setBorderWidth(1); globalBtn.setBorderColor(QMUIResHelper.getAttrColor(context, R.attr.qmui_skin_support_color_separator)); globalBtn.setBackgroundColor(QMUIResHelper.getAttrColor(context, R.attr.app_skin_common_background)); globalBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { showGlobalActionPopup(v); } }); FrameLayout.LayoutParams globalBtnLp = new FrameLayout.LayoutParams(btnSize, btnSize); globalBtnLp.gravity = Gravity.BOTTOM | Gravity.RIGHT; globalBtnLp.bottomMargin = QMUIDisplayHelper.dp2px(context, 60); globalBtnLp.rightMargin = QMUIDisplayHelper.dp2px(context, 24); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.background(R.attr.app_skin_common_background); builder.border(R.attr.qmui_skin_support_color_separator); builder.tintColor(R.attr.app_skin_common_img_tint_color); QMUISkinHelper.setSkinValue(globalBtn, builder); builder.release(); addView(globalBtn, globalBtnLp); globalBtnOffsetHelper = new QMUIViewOffsetHelper(globalBtn); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); globalBtnOffsetHelper.onViewLayout(); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { float x = event.getX(), y = event.getY(); int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { isTouchDownInGlobalBtn = isDownInGlobalBtn(x, y); touchDownX = lastTouchX = x; touchDownY = lastTouchY = y; } else if (action == MotionEvent.ACTION_MOVE) { if (!isDragging && isTouchDownInGlobalBtn) { int dx = (int) (x - touchDownX); int dy = (int) (y - touchDownY); if (Math.sqrt(dx * dx + dy * dy) > touchSlop) { isDragging = true; } } if (isDragging) { int dx = (int) (x - lastTouchX); int dy = (int) (y - lastTouchY); int gx = globalBtn.getLeft(); int gy = globalBtn.getTop(); int gw = globalBtn.getWidth(), w = getWidth(); int gh = globalBtn.getHeight(), h = getHeight(); if (gx + dx < 0) { dx = -gx; } else if (gx + dx + gw > w) { dx = w - gw - gx; } if (gy + dy < 0) { dy = -gy; } else if (gy + dy + gh > h) { dy = h - gh - gy; } globalBtnOffsetHelper.setLeftAndRightOffset( globalBtnOffsetHelper.getLeftAndRightOffset() + dx); globalBtnOffsetHelper.setTopAndBottomOffset( globalBtnOffsetHelper.getTopAndBottomOffset() + dy); } lastTouchX = x; lastTouchY = y; } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { isDragging = false; isTouchDownInGlobalBtn = false; } return isDragging; } private boolean isDownInGlobalBtn(float x, float y) { return globalBtn.getLeft() < x && globalBtn.getRight() > x && globalBtn.getTop() < y && globalBtn.getBottom() > y; } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(), y = event.getY(); int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { isTouchDownInGlobalBtn = isDownInGlobalBtn(x, y); touchDownX = lastTouchX = x; touchDownY = lastTouchY = y; } else if (action == MotionEvent.ACTION_MOVE) { if (!isDragging && isTouchDownInGlobalBtn) { int dx = (int) (x - touchDownX); int dy = (int) (y - touchDownY); if (Math.sqrt(dx * dx + dy * dy) > touchSlop) { isDragging = true; } } if (isDragging) { int dx = (int) (x - lastTouchX); int dy = (int) (y - lastTouchY); int gx = globalBtn.getLeft(); int gy = globalBtn.getTop(); int gw = globalBtn.getWidth(), w = getWidth(); int gh = globalBtn.getHeight(), h = getHeight(); if (gx + dx < 0) { dx = -gx; } else if (gx + dx + gw > w) { dx = w - gw - gx; } if (gy + dy < 0) { dy = -gy; } else if (gy + dy + gh > h) { dy = h - gh - gy; } globalBtnOffsetHelper.setLeftAndRightOffset( globalBtnOffsetHelper.getLeftAndRightOffset() + dx); globalBtnOffsetHelper.setTopAndBottomOffset( globalBtnOffsetHelper.getTopAndBottomOffset() + dy); } lastTouchX = x; lastTouchY = y; } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { isDragging = false; isTouchDownInGlobalBtn = false; } return isDragging || super.onTouchEvent(event); } @Override public FragmentContainerView getFragmentContainerView() { return fragmentContainer; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/QDQQFaceManager.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; import android.graphics.drawable.Drawable; import androidx.collection.ArrayMap; import androidx.core.content.ContextCompat; import android.util.Log; import android.util.SparseIntArray; import com.qmuiteam.qmui.qqface.IQMUIQQFaceManager; import com.qmuiteam.qmui.qqface.QQFace; import com.qmuiteam.qmui.type.parser.EmojiResourceProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * @author cginechen * @date 2016-12-21 */ public class QDQQFaceManager implements IQMUIQQFaceManager, EmojiResourceProvider { private static final HashMap sQQFaceMap = new HashMap<>(); private static final List mQQFaceList = new ArrayList<>(); private static final SparseIntArray sEmojisMap = new SparseIntArray(846); private static final SparseIntArray sSoftbanksMap = new SparseIntArray(471); @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private static final ArrayMap mQQFaceFileNameList = new ArrayMap<>();//存储QQ表情对应的文件名,方便混淆后可以获取到原文件名 private static QDQQFaceManager sQDQQFaceManager = new QDQQFaceManager(); static { long start = System.currentTimeMillis(); mQQFaceList.add(new QQFace("[微笑]", R.drawable.smiley_0)); mQQFaceList.add(new QQFace("[撇嘴]", R.drawable.smiley_1)); mQQFaceList.add(new QQFace("[色]", R.drawable.smiley_2)); mQQFaceList.add(new QQFace("[发呆]", R.drawable.smiley_3)); mQQFaceList.add(new QQFace("[得意]", R.drawable.smiley_4)); mQQFaceList.add(new QQFace("[流泪]", R.drawable.smiley_5)); mQQFaceList.add(new QQFace("[害羞]", R.drawable.smiley_6)); mQQFaceList.add(new QQFace("[闭嘴]", R.drawable.smiley_7)); mQQFaceList.add(new QQFace("[睡]", R.drawable.smiley_8)); mQQFaceList.add(new QQFace("[大哭]", R.drawable.smiley_9)); mQQFaceList.add(new QQFace("[尴尬]", R.drawable.smiley_10)); mQQFaceList.add(new QQFace("[发怒]", R.drawable.smiley_11)); mQQFaceList.add(new QQFace("[调皮]", R.drawable.smiley_12)); mQQFaceList.add(new QQFace("[呲牙]", R.drawable.smiley_13)); mQQFaceList.add(new QQFace("[惊讶]", R.drawable.smiley_14)); mQQFaceList.add(new QQFace("[难过]", R.drawable.smiley_15)); mQQFaceList.add(new QQFace("[酷]", R.drawable.smiley_16)); mQQFaceList.add(new QQFace("[冷汗]", R.drawable.smiley_17)); mQQFaceList.add(new QQFace("[抓狂]", R.drawable.smiley_18)); mQQFaceList.add(new QQFace("[吐]", R.drawable.smiley_19)); mQQFaceList.add(new QQFace("[偷笑]", R.drawable.smiley_20)); mQQFaceList.add(new QQFace("[可爱]", R.drawable.smiley_21)); mQQFaceList.add(new QQFace("[白眼]", R.drawable.smiley_22)); mQQFaceList.add(new QQFace("[傲慢]", R.drawable.smiley_23)); mQQFaceList.add(new QQFace("[饥饿]", R.drawable.smiley_24)); mQQFaceList.add(new QQFace("[困]", R.drawable.smiley_25)); mQQFaceList.add(new QQFace("[惊恐]", R.drawable.smiley_26)); mQQFaceList.add(new QQFace("[流汗]", R.drawable.smiley_27)); mQQFaceList.add(new QQFace("[憨笑]", R.drawable.smiley_28)); mQQFaceList.add(new QQFace("[大兵]", R.drawable.smiley_29)); mQQFaceList.add(new QQFace("[奋斗]", R.drawable.smiley_30)); mQQFaceList.add(new QQFace("[咒骂]", R.drawable.smiley_31)); mQQFaceList.add(new QQFace("[疑问]", R.drawable.smiley_32)); mQQFaceList.add(new QQFace("[嘘]", R.drawable.smiley_33)); mQQFaceList.add(new QQFace("[晕]", R.drawable.smiley_34)); mQQFaceList.add(new QQFace("[折磨]", R.drawable.smiley_35)); mQQFaceList.add(new QQFace("[衰]", R.drawable.smiley_36)); mQQFaceList.add(new QQFace("[骷髅]", R.drawable.smiley_37)); mQQFaceList.add(new QQFace("[敲打]", R.drawable.smiley_38)); mQQFaceList.add(new QQFace("[再见]", R.drawable.smiley_39)); mQQFaceList.add(new QQFace("[擦汗]", R.drawable.smiley_40)); mQQFaceList.add(new QQFace("[抠鼻]", R.drawable.smiley_41)); mQQFaceList.add(new QQFace("[鼓掌]", R.drawable.smiley_42)); mQQFaceList.add(new QQFace("[糗大了]", R.drawable.smiley_43)); mQQFaceList.add(new QQFace("[坏笑]", R.drawable.smiley_44)); mQQFaceList.add(new QQFace("[左哼哼]", R.drawable.smiley_45)); mQQFaceList.add(new QQFace("[右哼哼]", R.drawable.smiley_46)); mQQFaceList.add(new QQFace("[哈欠]", R.drawable.smiley_47)); mQQFaceList.add(new QQFace("[鄙视]", R.drawable.smiley_48)); mQQFaceList.add(new QQFace("[委屈]", R.drawable.smiley_49)); mQQFaceList.add(new QQFace("[快哭了]", R.drawable.smiley_50)); mQQFaceList.add(new QQFace("[阴险]", R.drawable.smiley_51)); mQQFaceList.add(new QQFace("[亲亲]", R.drawable.smiley_52)); mQQFaceList.add(new QQFace("[吓]", R.drawable.smiley_53)); mQQFaceList.add(new QQFace("[可怜]", R.drawable.smiley_54)); mQQFaceList.add(new QQFace("[菜刀]", R.drawable.smiley_55)); mQQFaceList.add(new QQFace("[西瓜]", R.drawable.smiley_56)); mQQFaceList.add(new QQFace("[啤酒]", R.drawable.smiley_57)); mQQFaceList.add(new QQFace("[篮球]", R.drawable.smiley_58)); mQQFaceList.add(new QQFace("[乒乓]", R.drawable.smiley_59)); mQQFaceList.add(new QQFace("[咖啡]", R.drawable.smiley_60)); mQQFaceList.add(new QQFace("[饭]", R.drawable.smiley_61)); mQQFaceList.add(new QQFace("[猪头]", R.drawable.smiley_62)); mQQFaceList.add(new QQFace("[玫瑰]", R.drawable.smiley_63)); mQQFaceList.add(new QQFace("[凋谢]", R.drawable.smiley_64)); mQQFaceList.add(new QQFace("[示爱]", R.drawable.smiley_65)); mQQFaceList.add(new QQFace("[爱心]", R.drawable.smiley_66)); mQQFaceList.add(new QQFace("[心碎]", R.drawable.smiley_67)); mQQFaceList.add(new QQFace("[蛋糕]", R.drawable.smiley_68)); mQQFaceList.add(new QQFace("[闪电]", R.drawable.smiley_69)); mQQFaceList.add(new QQFace("[炸弹]", R.drawable.smiley_70)); mQQFaceList.add(new QQFace("[刀]", R.drawable.smiley_71)); mQQFaceList.add(new QQFace("[足球]", R.drawable.smiley_72)); mQQFaceList.add(new QQFace("[瓢虫]", R.drawable.smiley_73)); mQQFaceList.add(new QQFace("[便便]", R.drawable.smiley_74)); mQQFaceList.add(new QQFace("[月亮]", R.drawable.smiley_75)); mQQFaceList.add(new QQFace("[太阳]", R.drawable.smiley_76)); mQQFaceList.add(new QQFace("[礼物]", R.drawable.smiley_77)); mQQFaceList.add(new QQFace("[拥抱]", R.drawable.smiley_78)); mQQFaceList.add(new QQFace("[强]", R.drawable.smiley_79)); mQQFaceList.add(new QQFace("[弱]", R.drawable.smiley_80)); mQQFaceList.add(new QQFace("[握手]", R.drawable.smiley_81)); mQQFaceList.add(new QQFace("[胜利]", R.drawable.smiley_82)); mQQFaceList.add(new QQFace("[抱拳]", R.drawable.smiley_83)); mQQFaceList.add(new QQFace("[勾引]", R.drawable.smiley_84)); mQQFaceList.add(new QQFace("[拳头]", R.drawable.smiley_85)); mQQFaceList.add(new QQFace("[差劲]", R.drawable.smiley_86)); mQQFaceList.add(new QQFace("[爱你]", R.drawable.smiley_87)); mQQFaceList.add(new QQFace("[NO]", R.drawable.smiley_88)); mQQFaceList.add(new QQFace("[OK]", R.drawable.smiley_89)); mQQFaceList.add(new QQFace("[爱情]", R.drawable.smiley_90)); mQQFaceList.add(new QQFace("[飞吻]", R.drawable.smiley_91)); mQQFaceList.add(new QQFace("[跳跳]", R.drawable.smiley_92)); mQQFaceList.add(new QQFace("[发抖]", R.drawable.smiley_93)); mQQFaceList.add(new QQFace("[怄火]", R.drawable.smiley_94)); mQQFaceList.add(new QQFace("[转圈]", R.drawable.smiley_95)); mQQFaceList.add(new QQFace("[磕头]", R.drawable.smiley_96)); mQQFaceList.add(new QQFace("[回头]", R.drawable.smiley_97)); mQQFaceList.add(new QQFace("[跳绳]", R.drawable.smiley_98)); mQQFaceList.add(new QQFace("[挥手]", R.drawable.smiley_99)); mQQFaceList.add(new QQFace("[激动]", R.drawable.smiley_100)); mQQFaceList.add(new QQFace("[街舞]", R.drawable.smiley_101)); mQQFaceList.add(new QQFace("[献吻]", R.drawable.smiley_102)); mQQFaceList.add(new QQFace("[左太极]", R.drawable.smiley_103)); mQQFaceList.add(new QQFace("[右太极]", R.drawable.smiley_104)); for (QQFace face : mQQFaceList) { sQQFaceMap.put(face.getName(), face.getRes()); } mQQFaceFileNameList.put("[微笑]", "smiley_0"); mQQFaceFileNameList.put("[撇嘴]", "smiley_1"); mQQFaceFileNameList.put("[色]", "smiley_2"); mQQFaceFileNameList.put("[发呆]", "smiley_3"); mQQFaceFileNameList.put("[得意]", "smiley_4"); mQQFaceFileNameList.put("[流泪]", "smiley_5"); mQQFaceFileNameList.put("[害羞]", "smiley_6"); mQQFaceFileNameList.put("[闭嘴]", "smiley_7"); mQQFaceFileNameList.put("[睡]", "smiley_8"); mQQFaceFileNameList.put("[大哭]", "smiley_9"); mQQFaceFileNameList.put("[尴尬]", "smiley_10"); mQQFaceFileNameList.put("[发怒]", "smiley_11"); mQQFaceFileNameList.put("[调皮]", "smiley_12"); mQQFaceFileNameList.put("[呲牙]", "smiley_13"); mQQFaceFileNameList.put("[惊讶]", "smiley_14"); mQQFaceFileNameList.put("[难过]", "smiley_15"); mQQFaceFileNameList.put("[酷]", "smiley_16"); mQQFaceFileNameList.put("[冷汗]", "smiley_17"); mQQFaceFileNameList.put("[抓狂]", "smiley_18"); mQQFaceFileNameList.put("[吐]", "smiley_19"); mQQFaceFileNameList.put("[偷笑]", "smiley_20"); mQQFaceFileNameList.put("[可爱]", "smiley_21"); mQQFaceFileNameList.put("[白眼]", "smiley_22"); mQQFaceFileNameList.put("[傲慢]", "smiley_23"); mQQFaceFileNameList.put("[饥饿]", "smiley_24"); mQQFaceFileNameList.put("[困]", "smiley_25"); mQQFaceFileNameList.put("[惊恐]", "smiley_26"); mQQFaceFileNameList.put("[流汗]", "smiley_27"); mQQFaceFileNameList.put("[憨笑]", "smiley_28"); mQQFaceFileNameList.put("[大兵]", "smiley_29"); mQQFaceFileNameList.put("[奋斗]", "smiley_30"); mQQFaceFileNameList.put("[咒骂]", "smiley_31"); mQQFaceFileNameList.put("[疑问]", "smiley_32"); mQQFaceFileNameList.put("[嘘]", "smiley_33"); mQQFaceFileNameList.put("[晕]", "smiley_34"); mQQFaceFileNameList.put("[折磨]", "smiley_35"); mQQFaceFileNameList.put("[衰]", "smiley_36"); mQQFaceFileNameList.put("[骷髅]", "smiley_37"); mQQFaceFileNameList.put("[敲打]", "smiley_38"); mQQFaceFileNameList.put("[再见]", "smiley_39"); mQQFaceFileNameList.put("[擦汗]", "smiley_40"); mQQFaceFileNameList.put("[抠鼻]", "smiley_41"); mQQFaceFileNameList.put("[鼓掌]", "smiley_42"); mQQFaceFileNameList.put("[糗大了]", "smiley_43"); mQQFaceFileNameList.put("[坏笑]", "smiley_44"); mQQFaceFileNameList.put("[左哼哼]", "smiley_45"); mQQFaceFileNameList.put("[右哼哼]", "smiley_46"); mQQFaceFileNameList.put("[哈欠]", "smiley_47"); mQQFaceFileNameList.put("[鄙视]", "smiley_48"); mQQFaceFileNameList.put("[委屈]", "smiley_49"); mQQFaceFileNameList.put("[快哭了]", "smiley_50"); mQQFaceFileNameList.put("[阴险]", "smiley_51"); mQQFaceFileNameList.put("[亲亲]", "smiley_52"); mQQFaceFileNameList.put("[吓]", "smiley_53"); mQQFaceFileNameList.put("[可怜]", "smiley_54"); mQQFaceFileNameList.put("[菜刀]", "smiley_55"); mQQFaceFileNameList.put("[西瓜]", "smiley_56"); mQQFaceFileNameList.put("[啤酒]", "smiley_57"); mQQFaceFileNameList.put("[篮球]", "smiley_58"); mQQFaceFileNameList.put("[乒乓]", "smiley_59"); mQQFaceFileNameList.put("[咖啡]", "smiley_60"); mQQFaceFileNameList.put("[饭]", "smiley_61"); mQQFaceFileNameList.put("[猪头]", "smiley_62"); mQQFaceFileNameList.put("[玫瑰]", "smiley_63"); mQQFaceFileNameList.put("[凋谢]", "smiley_64"); mQQFaceFileNameList.put("[示爱]", "smiley_65"); mQQFaceFileNameList.put("[爱心]", "smiley_66"); mQQFaceFileNameList.put("[心碎]", "smiley_67"); mQQFaceFileNameList.put("[蛋糕]", "smiley_68"); mQQFaceFileNameList.put("[闪电]", "smiley_69"); mQQFaceFileNameList.put("[炸弹]", "smiley_70"); mQQFaceFileNameList.put("[刀]", "smiley_71"); mQQFaceFileNameList.put("[足球]", "smiley_72"); mQQFaceFileNameList.put("[瓢虫]", "smiley_73"); mQQFaceFileNameList.put("[便便]", "smiley_74"); mQQFaceFileNameList.put("[月亮]", "smiley_75"); mQQFaceFileNameList.put("[太阳]", "smiley_76"); mQQFaceFileNameList.put("[礼物]", "smiley_77"); mQQFaceFileNameList.put("[拥抱]", "smiley_78"); mQQFaceFileNameList.put("[强]", "smiley_79"); mQQFaceFileNameList.put("[弱]", "smiley_80"); mQQFaceFileNameList.put("[握手]", "smiley_81"); mQQFaceFileNameList.put("[胜利]", "smiley_82"); mQQFaceFileNameList.put("[抱拳]", "smiley_83"); mQQFaceFileNameList.put("[勾引]", "smiley_84"); mQQFaceFileNameList.put("[拳头]", "smiley_85"); mQQFaceFileNameList.put("[差劲]", "smiley_86"); mQQFaceFileNameList.put("[爱你]", "smiley_87"); mQQFaceFileNameList.put("[NO]", "smiley_88"); mQQFaceFileNameList.put("[OK]", "smiley_89"); mQQFaceFileNameList.put("[爱情]", "smiley_90"); mQQFaceFileNameList.put("[飞吻]", "smiley_91"); mQQFaceFileNameList.put("[跳跳]", "smiley_92"); mQQFaceFileNameList.put("[发抖]", "smiley_93"); mQQFaceFileNameList.put("[怄火]", "smiley_94"); mQQFaceFileNameList.put("[转圈]", "smiley_95"); mQQFaceFileNameList.put("[磕头]", "smiley_96"); mQQFaceFileNameList.put("[回头]", "smiley_97"); mQQFaceFileNameList.put("[跳绳]", "smiley_98"); mQQFaceFileNameList.put("[挥手]", "smiley_99"); mQQFaceFileNameList.put("[激动]", "smiley_100"); mQQFaceFileNameList.put("[街舞]", "smiley_101"); mQQFaceFileNameList.put("[献吻]", "smiley_102"); mQQFaceFileNameList.put("[左太极]", "smiley_103"); mQQFaceFileNameList.put("[右太极]", "smiley_104"); sEmojisMap.append(0x00a9, R.drawable.emoji_00a9); sEmojisMap.append(0x00ae, R.drawable.emoji_00ae); sEmojisMap.append(0x203c, R.drawable.emoji_203c); sEmojisMap.append(0x2049, R.drawable.emoji_2049); sEmojisMap.append(0x2122, R.drawable.emoji_2122); sEmojisMap.append(0x2139, R.drawable.emoji_2139); sEmojisMap.append(0x2194, R.drawable.emoji_2194); sEmojisMap.append(0x2195, R.drawable.emoji_2195); sEmojisMap.append(0x2196, R.drawable.emoji_2196); sEmojisMap.append(0x2197, R.drawable.emoji_2197); sEmojisMap.append(0x2198, R.drawable.emoji_2198); sEmojisMap.append(0x2199, R.drawable.emoji_2199); sEmojisMap.append(0x21a9, R.drawable.emoji_21a9); sEmojisMap.append(0x21aa, R.drawable.emoji_21aa); sEmojisMap.append(0x231a, R.drawable.emoji_231a); sEmojisMap.append(0x231b, R.drawable.emoji_231b); sEmojisMap.append(0x23e9, R.drawable.emoji_23e9); sEmojisMap.append(0x23ea, R.drawable.emoji_23ea); sEmojisMap.append(0x23eb, R.drawable.emoji_23eb); sEmojisMap.append(0x23ec, R.drawable.emoji_23ec); sEmojisMap.append(0x23f0, R.drawable.emoji_23f0); sEmojisMap.append(0x23f3, R.drawable.emoji_23f3); sEmojisMap.append(0x24c2, R.drawable.emoji_24c2); sEmojisMap.append(0x25aa, R.drawable.emoji_25aa); sEmojisMap.append(0x25ab, R.drawable.emoji_25ab); sEmojisMap.append(0x25b6, R.drawable.emoji_25b6); sEmojisMap.append(0x25c0, R.drawable.emoji_25c0); sEmojisMap.append(0x25fb, R.drawable.emoji_25fb); sEmojisMap.append(0x25fc, R.drawable.emoji_25fc); sEmojisMap.append(0x25fd, R.drawable.emoji_25fd); sEmojisMap.append(0x25fe, R.drawable.emoji_25fe); sEmojisMap.append(0x2600, R.drawable.emoji_2600); sEmojisMap.append(0x2601, R.drawable.emoji_2601); sEmojisMap.append(0x260e, R.drawable.emoji_260e); sEmojisMap.append(0x2611, R.drawable.emoji_2611); sEmojisMap.append(0x2614, R.drawable.emoji_2614); sEmojisMap.append(0x2615, R.drawable.emoji_2615); sEmojisMap.append(0x261d, R.drawable.emoji_261d); sEmojisMap.append(0x263a, R.drawable.emoji_263a); sEmojisMap.append(0x2648, R.drawable.emoji_2648); sEmojisMap.append(0x2649, R.drawable.emoji_2649); sEmojisMap.append(0x264a, R.drawable.emoji_264a); sEmojisMap.append(0x264b, R.drawable.emoji_264b); sEmojisMap.append(0x264c, R.drawable.emoji_264c); sEmojisMap.append(0x264d, R.drawable.emoji_264d); sEmojisMap.append(0x264e, R.drawable.emoji_264e); sEmojisMap.append(0x264f, R.drawable.emoji_264f); sEmojisMap.append(0x2650, R.drawable.emoji_2650); sEmojisMap.append(0x2651, R.drawable.emoji_2651); sEmojisMap.append(0x2652, R.drawable.emoji_2652); sEmojisMap.append(0x2653, R.drawable.emoji_2653); sEmojisMap.append(0x2660, R.drawable.emoji_2660); sEmojisMap.append(0x2663, R.drawable.emoji_2663); sEmojisMap.append(0x2665, R.drawable.emoji_2665); sEmojisMap.append(0x2666, R.drawable.emoji_2666); sEmojisMap.append(0x2668, R.drawable.emoji_2668); sEmojisMap.append(0x267b, R.drawable.emoji_267b); sEmojisMap.append(0x267f, R.drawable.emoji_267f); sEmojisMap.append(0x2693, R.drawable.emoji_2693); sEmojisMap.append(0x26a0, R.drawable.emoji_26a0); sEmojisMap.append(0x26a1, R.drawable.emoji_26a1); sEmojisMap.append(0x26aa, R.drawable.emoji_26aa); sEmojisMap.append(0x26ab, R.drawable.emoji_26ab); sEmojisMap.append(0x26bd, R.drawable.emoji_26bd); sEmojisMap.append(0x26be, R.drawable.emoji_26be); sEmojisMap.append(0x26c4, R.drawable.emoji_26c4); sEmojisMap.append(0x26c5, R.drawable.emoji_26c5); sEmojisMap.append(0x26ce, R.drawable.emoji_26ce); sEmojisMap.append(0x26d4, R.drawable.emoji_26d4); sEmojisMap.append(0x26ea, R.drawable.emoji_26ea); sEmojisMap.append(0x26f2, R.drawable.emoji_26f2); sEmojisMap.append(0x26f3, R.drawable.emoji_26f3); sEmojisMap.append(0x26f5, R.drawable.emoji_26f5); sEmojisMap.append(0x26fa, R.drawable.emoji_26fa); sEmojisMap.append(0x26fd, R.drawable.emoji_26fd); sEmojisMap.append(0x2702, R.drawable.emoji_2702); sEmojisMap.append(0x2705, R.drawable.emoji_2705); sEmojisMap.append(0x2708, R.drawable.emoji_2708); sEmojisMap.append(0x2709, R.drawable.emoji_2709); sEmojisMap.append(0x270a, R.drawable.emoji_270a); sEmojisMap.append(0x270b, R.drawable.emoji_270b); sEmojisMap.append(0x270c, R.drawable.emoji_270c); sEmojisMap.append(0x270f, R.drawable.emoji_270f); sEmojisMap.append(0x2712, R.drawable.emoji_2712); sEmojisMap.append(0x2714, R.drawable.emoji_2714); sEmojisMap.append(0x2716, R.drawable.emoji_2716); sEmojisMap.append(0x2728, R.drawable.emoji_2728); sEmojisMap.append(0x2733, R.drawable.emoji_2733); sEmojisMap.append(0x2734, R.drawable.emoji_2734); sEmojisMap.append(0x2744, R.drawable.emoji_2744); sEmojisMap.append(0x2747, R.drawable.emoji_2747); sEmojisMap.append(0x274c, R.drawable.emoji_274c); sEmojisMap.append(0x274e, R.drawable.emoji_274e); sEmojisMap.append(0x2753, R.drawable.emoji_2753); sEmojisMap.append(0x2754, R.drawable.emoji_2754); sEmojisMap.append(0x2755, R.drawable.emoji_2755); sEmojisMap.append(0x2757, R.drawable.emoji_2757); sEmojisMap.append(0x2764, R.drawable.emoji_2764); sEmojisMap.append(0x2795, R.drawable.emoji_2795); sEmojisMap.append(0x2796, R.drawable.emoji_2796); sEmojisMap.append(0x2797, R.drawable.emoji_2797); sEmojisMap.append(0x27a1, R.drawable.emoji_27a1); sEmojisMap.append(0x27b0, R.drawable.emoji_27b0); sEmojisMap.append(0x27bf, R.drawable.emoji_27bf); sEmojisMap.append(0x2934, R.drawable.emoji_2934); sEmojisMap.append(0x2935, R.drawable.emoji_2935); sEmojisMap.append(0x2b05, R.drawable.emoji_2b05); sEmojisMap.append(0x2b06, R.drawable.emoji_2b06); sEmojisMap.append(0x2b07, R.drawable.emoji_2b07); sEmojisMap.append(0x2b1b, R.drawable.emoji_2b1b); sEmojisMap.append(0x2b1c, R.drawable.emoji_2b1c); sEmojisMap.append(0x2b50, R.drawable.emoji_2b50); sEmojisMap.append(0x2b55, R.drawable.emoji_2b55); sEmojisMap.append(0x3030, R.drawable.emoji_3030); sEmojisMap.append(0x303d, R.drawable.emoji_303d); sEmojisMap.append(0x3297, R.drawable.emoji_3297); sEmojisMap.append(0x3299, R.drawable.emoji_3299); sEmojisMap.append(0x1f004, R.drawable.emoji_1f004); sEmojisMap.append(0x1f0cf, R.drawable.emoji_1f0cf); sEmojisMap.append(0x1f170, R.drawable.emoji_1f170); sEmojisMap.append(0x1f171, R.drawable.emoji_1f171); sEmojisMap.append(0x1f17e, R.drawable.emoji_1f17e); sEmojisMap.append(0x1f17f, R.drawable.emoji_1f17f); sEmojisMap.append(0x1f18e, R.drawable.emoji_1f18e); sEmojisMap.append(0x1f191, R.drawable.emoji_1f191); sEmojisMap.append(0x1f192, R.drawable.emoji_1f192); sEmojisMap.append(0x1f193, R.drawable.emoji_1f193); sEmojisMap.append(0x1f194, R.drawable.emoji_1f194); sEmojisMap.append(0x1f195, R.drawable.emoji_1f195); sEmojisMap.append(0x1f196, R.drawable.emoji_1f196); sEmojisMap.append(0x1f197, R.drawable.emoji_1f197); sEmojisMap.append(0x1f198, R.drawable.emoji_1f198); sEmojisMap.append(0x1f199, R.drawable.emoji_1f199); sEmojisMap.append(0x1f19a, R.drawable.emoji_1f19a); sEmojisMap.append(0x1f201, R.drawable.emoji_1f201); sEmojisMap.append(0x1f202, R.drawable.emoji_1f202); sEmojisMap.append(0x1f21a, R.drawable.emoji_1f21a); sEmojisMap.append(0x1f22f, R.drawable.emoji_1f22f); sEmojisMap.append(0x1f232, R.drawable.emoji_1f232); sEmojisMap.append(0x1f233, R.drawable.emoji_1f233); sEmojisMap.append(0x1f234, R.drawable.emoji_1f234); sEmojisMap.append(0x1f235, R.drawable.emoji_1f235); sEmojisMap.append(0x1f236, R.drawable.emoji_1f236); sEmojisMap.append(0x1f237, R.drawable.emoji_1f237); sEmojisMap.append(0x1f238, R.drawable.emoji_1f238); sEmojisMap.append(0x1f239, R.drawable.emoji_1f239); sEmojisMap.append(0x1f23a, R.drawable.emoji_1f23a); sEmojisMap.append(0x1f250, R.drawable.emoji_1f250); sEmojisMap.append(0x1f251, R.drawable.emoji_1f251); sEmojisMap.append(0x1f300, R.drawable.emoji_1f300); sEmojisMap.append(0x1f301, R.drawable.emoji_1f301); sEmojisMap.append(0x1f302, R.drawable.emoji_1f302); sEmojisMap.append(0x1f303, R.drawable.emoji_1f303); sEmojisMap.append(0x1f304, R.drawable.emoji_1f304); sEmojisMap.append(0x1f305, R.drawable.emoji_1f305); sEmojisMap.append(0x1f306, R.drawable.emoji_1f306); sEmojisMap.append(0x1f307, R.drawable.emoji_1f307); sEmojisMap.append(0x1f308, R.drawable.emoji_1f308); sEmojisMap.append(0x1f309, R.drawable.emoji_1f309); sEmojisMap.append(0x1f30a, R.drawable.emoji_1f30a); sEmojisMap.append(0x1f30b, R.drawable.emoji_1f30b); sEmojisMap.append(0x1f30c, R.drawable.emoji_1f30c); sEmojisMap.append(0x1f30d, R.drawable.emoji_1f30d); sEmojisMap.append(0x1f30e, R.drawable.emoji_1f30e); sEmojisMap.append(0x1f30f, R.drawable.emoji_1f30f); sEmojisMap.append(0x1f310, R.drawable.emoji_1f310); sEmojisMap.append(0x1f311, R.drawable.emoji_1f311); sEmojisMap.append(0x1f312, R.drawable.emoji_1f312); sEmojisMap.append(0x1f313, R.drawable.emoji_1f313); sEmojisMap.append(0x1f314, R.drawable.emoji_1f314); sEmojisMap.append(0x1f315, R.drawable.emoji_1f315); sEmojisMap.append(0x1f316, R.drawable.emoji_1f316); sEmojisMap.append(0x1f317, R.drawable.emoji_1f317); sEmojisMap.append(0x1f318, R.drawable.emoji_1f318); sEmojisMap.append(0x1f319, R.drawable.emoji_1f319); sEmojisMap.append(0x1f31a, R.drawable.emoji_1f31a); sEmojisMap.append(0x1f31b, R.drawable.emoji_1f31b); sEmojisMap.append(0x1f31c, R.drawable.emoji_1f31c); sEmojisMap.append(0x1f31d, R.drawable.emoji_1f31d); sEmojisMap.append(0x1f31e, R.drawable.emoji_1f31e); sEmojisMap.append(0x1f31f, R.drawable.emoji_1f31f); sEmojisMap.append(0x1f320, R.drawable.emoji_1f303); sEmojisMap.append(0x1f330, R.drawable.emoji_1f330); sEmojisMap.append(0x1f331, R.drawable.emoji_1f331); sEmojisMap.append(0x1f332, R.drawable.emoji_1f332); sEmojisMap.append(0x1f333, R.drawable.emoji_1f333); sEmojisMap.append(0x1f334, R.drawable.emoji_1f334); sEmojisMap.append(0x1f335, R.drawable.emoji_1f335); sEmojisMap.append(0x1f337, R.drawable.emoji_1f337); sEmojisMap.append(0x1f338, R.drawable.emoji_1f338); sEmojisMap.append(0x1f339, R.drawable.emoji_1f339); sEmojisMap.append(0x1f33a, R.drawable.emoji_1f33a); sEmojisMap.append(0x1f33b, R.drawable.emoji_1f33b); sEmojisMap.append(0x1f33c, R.drawable.emoji_1f33c); sEmojisMap.append(0x1f33d, R.drawable.emoji_1f33d); sEmojisMap.append(0x1f33e, R.drawable.emoji_1f33e); sEmojisMap.append(0x1f33f, R.drawable.emoji_1f33f); sEmojisMap.append(0x1f340, R.drawable.emoji_1f340); sEmojisMap.append(0x1f341, R.drawable.emoji_1f341); sEmojisMap.append(0x1f342, R.drawable.emoji_1f342); sEmojisMap.append(0x1f343, R.drawable.emoji_1f343); sEmojisMap.append(0x1f344, R.drawable.emoji_1f344); sEmojisMap.append(0x1f345, R.drawable.emoji_1f345); sEmojisMap.append(0x1f346, R.drawable.emoji_1f346); sEmojisMap.append(0x1f347, R.drawable.emoji_1f347); sEmojisMap.append(0x1f348, R.drawable.emoji_1f348); sEmojisMap.append(0x1f349, R.drawable.emoji_1f349); sEmojisMap.append(0x1f34a, R.drawable.emoji_1f34a); sEmojisMap.append(0x1f34b, R.drawable.emoji_1f34b); sEmojisMap.append(0x1f34c, R.drawable.emoji_1f34c); sEmojisMap.append(0x1f34d, R.drawable.emoji_1f34d); sEmojisMap.append(0x1f34e, R.drawable.emoji_1f34e); sEmojisMap.append(0x1f34f, R.drawable.emoji_1f34f); sEmojisMap.append(0x1f350, R.drawable.emoji_1f350); sEmojisMap.append(0x1f351, R.drawable.emoji_1f351); sEmojisMap.append(0x1f352, R.drawable.emoji_1f352); sEmojisMap.append(0x1f353, R.drawable.emoji_1f353); sEmojisMap.append(0x1f354, R.drawable.emoji_1f354); sEmojisMap.append(0x1f355, R.drawable.emoji_1f355); sEmojisMap.append(0x1f356, R.drawable.emoji_1f356); sEmojisMap.append(0x1f357, R.drawable.emoji_1f357); sEmojisMap.append(0x1f358, R.drawable.emoji_1f358); sEmojisMap.append(0x1f359, R.drawable.emoji_1f359); sEmojisMap.append(0x1f35a, R.drawable.emoji_1f35a); sEmojisMap.append(0x1f35b, R.drawable.emoji_1f35b); sEmojisMap.append(0x1f35c, R.drawable.emoji_1f35c); sEmojisMap.append(0x1f35d, R.drawable.emoji_1f35d); sEmojisMap.append(0x1f35e, R.drawable.emoji_1f35e); sEmojisMap.append(0x1f35f, R.drawable.emoji_1f35f); sEmojisMap.append(0x1f360, R.drawable.emoji_1f360); sEmojisMap.append(0x1f361, R.drawable.emoji_1f361); sEmojisMap.append(0x1f362, R.drawable.emoji_1f362); sEmojisMap.append(0x1f363, R.drawable.emoji_1f363); sEmojisMap.append(0x1f364, R.drawable.emoji_1f364); sEmojisMap.append(0x1f365, R.drawable.emoji_1f365); sEmojisMap.append(0x1f366, R.drawable.emoji_1f366); sEmojisMap.append(0x1f367, R.drawable.emoji_1f367); sEmojisMap.append(0x1f368, R.drawable.emoji_1f368); sEmojisMap.append(0x1f369, R.drawable.emoji_1f369); sEmojisMap.append(0x1f36a, R.drawable.emoji_1f36a); sEmojisMap.append(0x1f36b, R.drawable.emoji_1f36b); sEmojisMap.append(0x1f36c, R.drawable.emoji_1f36c); sEmojisMap.append(0x1f36d, R.drawable.emoji_1f36d); sEmojisMap.append(0x1f36e, R.drawable.emoji_1f36e); sEmojisMap.append(0x1f36f, R.drawable.emoji_1f36f); sEmojisMap.append(0x1f370, R.drawable.emoji_1f370); sEmojisMap.append(0x1f371, R.drawable.emoji_1f371); sEmojisMap.append(0x1f372, R.drawable.emoji_1f372); sEmojisMap.append(0x1f373, R.drawable.emoji_1f373); sEmojisMap.append(0x1f374, R.drawable.emoji_1f374); sEmojisMap.append(0x1f375, R.drawable.emoji_1f375); sEmojisMap.append(0x1f376, R.drawable.emoji_1f376); sEmojisMap.append(0x1f377, R.drawable.emoji_1f377); sEmojisMap.append(0x1f378, R.drawable.emoji_1f378); sEmojisMap.append(0x1f379, R.drawable.emoji_1f379); sEmojisMap.append(0x1f37a, R.drawable.emoji_1f37a); sEmojisMap.append(0x1f37b, R.drawable.emoji_1f37b); sEmojisMap.append(0x1f37c, R.drawable.emoji_1f37c); sEmojisMap.append(0x1f380, R.drawable.emoji_1f380); sEmojisMap.append(0x1f381, R.drawable.emoji_1f381); sEmojisMap.append(0x1f382, R.drawable.emoji_1f382); sEmojisMap.append(0x1f383, R.drawable.emoji_1f383); sEmojisMap.append(0x1f384, R.drawable.emoji_1f384); sEmojisMap.append(0x1f385, R.drawable.emoji_1f385); sEmojisMap.append(0x1f386, R.drawable.emoji_1f386); sEmojisMap.append(0x1f387, R.drawable.emoji_1f387); sEmojisMap.append(0x1f388, R.drawable.emoji_1f388); sEmojisMap.append(0x1f389, R.drawable.emoji_1f389); sEmojisMap.append(0x1f38a, R.drawable.emoji_1f38a); sEmojisMap.append(0x1f38b, R.drawable.emoji_1f38b); sEmojisMap.append(0x1f38c, R.drawable.emoji_1f38c); sEmojisMap.append(0x1f38d, R.drawable.emoji_1f38d); sEmojisMap.append(0x1f38e, R.drawable.emoji_1f38e); sEmojisMap.append(0x1f38f, R.drawable.emoji_1f38f); sEmojisMap.append(0x1f390, R.drawable.emoji_1f390); sEmojisMap.append(0x1f391, R.drawable.emoji_1f391); sEmojisMap.append(0x1f392, R.drawable.emoji_1f392); sEmojisMap.append(0x1f393, R.drawable.emoji_1f393); sEmojisMap.append(0x1f3a0, R.drawable.emoji_1f3a0); sEmojisMap.append(0x1f3a1, R.drawable.emoji_1f3a1); sEmojisMap.append(0x1f3a2, R.drawable.emoji_1f3a2); sEmojisMap.append(0x1f3a3, R.drawable.emoji_1f3a3); sEmojisMap.append(0x1f3a4, R.drawable.emoji_1f3a4); sEmojisMap.append(0x1f3a5, R.drawable.emoji_1f3a5); sEmojisMap.append(0x1f3a6, R.drawable.emoji_1f3a6); sEmojisMap.append(0x1f3a7, R.drawable.emoji_1f3a7); sEmojisMap.append(0x1f3a8, R.drawable.emoji_1f3a8); sEmojisMap.append(0x1f3a9, R.drawable.emoji_1f3a9); sEmojisMap.append(0x1f3aa, R.drawable.emoji_1f3aa); sEmojisMap.append(0x1f3ab, R.drawable.emoji_1f3ab); sEmojisMap.append(0x1f3ac, R.drawable.emoji_1f3ac); sEmojisMap.append(0x1f3ad, R.drawable.emoji_1f3ad); sEmojisMap.append(0x1f3ae, R.drawable.emoji_1f3ae); sEmojisMap.append(0x1f3af, R.drawable.emoji_1f3af); sEmojisMap.append(0x1f3b0, R.drawable.emoji_1f3b0); sEmojisMap.append(0x1f3b1, R.drawable.emoji_1f3b1); sEmojisMap.append(0x1f3b2, R.drawable.emoji_1f3b2); sEmojisMap.append(0x1f3b3, R.drawable.emoji_1f3b3); sEmojisMap.append(0x1f3b4, R.drawable.emoji_1f3b4); sEmojisMap.append(0x1f3b5, R.drawable.emoji_1f3b5); sEmojisMap.append(0x1f3b6, R.drawable.emoji_1f3b6); sEmojisMap.append(0x1f3b7, R.drawable.emoji_1f3b7); sEmojisMap.append(0x1f3b8, R.drawable.emoji_1f3b8); sEmojisMap.append(0x1f3b9, R.drawable.emoji_1f3b9); sEmojisMap.append(0x1f3ba, R.drawable.emoji_1f3ba); sEmojisMap.append(0x1f3bb, R.drawable.emoji_1f3bb); sEmojisMap.append(0x1f3bc, R.drawable.emoji_1f3bc); sEmojisMap.append(0x1f3bd, R.drawable.emoji_1f3bd); sEmojisMap.append(0x1f3be, R.drawable.emoji_1f3be); sEmojisMap.append(0x1f3bf, R.drawable.emoji_1f3bf); sEmojisMap.append(0x1f3c0, R.drawable.emoji_1f3c0); sEmojisMap.append(0x1f3c1, R.drawable.emoji_1f3c1); sEmojisMap.append(0x1f3c2, R.drawable.emoji_1f3c2); sEmojisMap.append(0x1f3c3, R.drawable.emoji_1f3c3); sEmojisMap.append(0x1f3c4, R.drawable.emoji_1f3c4); sEmojisMap.append(0x1f3c6, R.drawable.emoji_1f3c6); sEmojisMap.append(0x1f3c7, R.drawable.emoji_1f3c7); sEmojisMap.append(0x1f3c8, R.drawable.emoji_1f3c8); sEmojisMap.append(0x1f3c9, R.drawable.emoji_1f3c9); sEmojisMap.append(0x1f3ca, R.drawable.emoji_1f3ca); sEmojisMap.append(0x1f3e0, R.drawable.emoji_1f3e0); sEmojisMap.append(0x1f3e1, R.drawable.emoji_1f3e1); sEmojisMap.append(0x1f3e2, R.drawable.emoji_1f3e2); sEmojisMap.append(0x1f3e3, R.drawable.emoji_1f3e3); sEmojisMap.append(0x1f3e4, R.drawable.emoji_1f3e4); sEmojisMap.append(0x1f3e5, R.drawable.emoji_1f3e5); sEmojisMap.append(0x1f3e6, R.drawable.emoji_1f3e6); sEmojisMap.append(0x1f3e7, R.drawable.emoji_1f3e7); sEmojisMap.append(0x1f3e8, R.drawable.emoji_1f3e8); sEmojisMap.append(0x1f3e9, R.drawable.emoji_1f3e9); sEmojisMap.append(0x1f3ea, R.drawable.emoji_1f3ea); sEmojisMap.append(0x1f3eb, R.drawable.emoji_1f3eb); sEmojisMap.append(0x1f3ec, R.drawable.emoji_1f3ec); sEmojisMap.append(0x1f3ed, R.drawable.emoji_1f3ed); sEmojisMap.append(0x1f3ee, R.drawable.emoji_1f3ee); sEmojisMap.append(0x1f3ef, R.drawable.emoji_1f3ef); sEmojisMap.append(0x1f3f0, R.drawable.emoji_1f3f0); sEmojisMap.append(0x1f400, R.drawable.emoji_1f400); sEmojisMap.append(0x1f401, R.drawable.emoji_1f401); sEmojisMap.append(0x1f402, R.drawable.emoji_1f402); sEmojisMap.append(0x1f403, R.drawable.emoji_1f403); sEmojisMap.append(0x1f404, R.drawable.emoji_1f404); sEmojisMap.append(0x1f405, R.drawable.emoji_1f405); sEmojisMap.append(0x1f406, R.drawable.emoji_1f406); sEmojisMap.append(0x1f407, R.drawable.emoji_1f407); sEmojisMap.append(0x1f408, R.drawable.emoji_1f408); sEmojisMap.append(0x1f409, R.drawable.emoji_1f409); sEmojisMap.append(0x1f40a, R.drawable.emoji_1f40a); sEmojisMap.append(0x1f40b, R.drawable.emoji_1f40b); sEmojisMap.append(0x1f40c, R.drawable.emoji_1f40c); sEmojisMap.append(0x1f40d, R.drawable.emoji_1f40d); sEmojisMap.append(0x1f40e, R.drawable.emoji_1f40e); sEmojisMap.append(0x1f40f, R.drawable.emoji_1f40f); sEmojisMap.append(0x1f410, R.drawable.emoji_1f410); sEmojisMap.append(0x1f411, R.drawable.emoji_1f411); sEmojisMap.append(0x1f412, R.drawable.emoji_1f412); sEmojisMap.append(0x1f413, R.drawable.emoji_1f413); sEmojisMap.append(0x1f414, R.drawable.emoji_1f414); sEmojisMap.append(0x1f415, R.drawable.emoji_1f415); sEmojisMap.append(0x1f416, R.drawable.emoji_1f416); sEmojisMap.append(0x1f417, R.drawable.emoji_1f417); sEmojisMap.append(0x1f418, R.drawable.emoji_1f418); sEmojisMap.append(0x1f419, R.drawable.emoji_1f419); sEmojisMap.append(0x1f41a, R.drawable.emoji_1f41a); sEmojisMap.append(0x1f41b, R.drawable.emoji_1f41b); sEmojisMap.append(0x1f41c, R.drawable.emoji_1f41c); sEmojisMap.append(0x1f41d, R.drawable.emoji_1f41d); sEmojisMap.append(0x1f41e, R.drawable.emoji_1f41e); sEmojisMap.append(0x1f41f, R.drawable.emoji_1f41f); sEmojisMap.append(0x1f420, R.drawable.emoji_1f420); sEmojisMap.append(0x1f421, R.drawable.emoji_1f421); sEmojisMap.append(0x1f422, R.drawable.emoji_1f422); sEmojisMap.append(0x1f423, R.drawable.emoji_1f423); sEmojisMap.append(0x1f424, R.drawable.emoji_1f424); sEmojisMap.append(0x1f425, R.drawable.emoji_1f425); sEmojisMap.append(0x1f426, R.drawable.emoji_1f426); sEmojisMap.append(0x1f427, R.drawable.emoji_1f427); sEmojisMap.append(0x1f428, R.drawable.emoji_1f428); sEmojisMap.append(0x1f429, R.drawable.emoji_1f429); sEmojisMap.append(0x1f42a, R.drawable.emoji_1f42a); sEmojisMap.append(0x1f42b, R.drawable.emoji_1f42b); sEmojisMap.append(0x1f42c, R.drawable.emoji_1f42c); sEmojisMap.append(0x1f42d, R.drawable.emoji_1f42d); sEmojisMap.append(0x1f42e, R.drawable.emoji_1f42e); sEmojisMap.append(0x1f42f, R.drawable.emoji_1f42f); sEmojisMap.append(0x1f430, R.drawable.emoji_1f430); sEmojisMap.append(0x1f431, R.drawable.emoji_1f431); sEmojisMap.append(0x1f432, R.drawable.emoji_1f432); sEmojisMap.append(0x1f433, R.drawable.emoji_1f433); sEmojisMap.append(0x1f434, R.drawable.emoji_1f434); sEmojisMap.append(0x1f435, R.drawable.emoji_1f435); sEmojisMap.append(0x1f436, R.drawable.emoji_1f436); sEmojisMap.append(0x1f437, R.drawable.emoji_1f437); sEmojisMap.append(0x1f438, R.drawable.emoji_1f438); sEmojisMap.append(0x1f439, R.drawable.emoji_1f439); sEmojisMap.append(0x1f43a, R.drawable.emoji_1f43a); sEmojisMap.append(0x1f43b, R.drawable.emoji_1f43b); sEmojisMap.append(0x1f43c, R.drawable.emoji_1f43c); sEmojisMap.append(0x1f43d, R.drawable.emoji_1f43d); sEmojisMap.append(0x1f43e, R.drawable.emoji_1f43e); sEmojisMap.append(0x1f440, R.drawable.emoji_1f440); sEmojisMap.append(0x1f442, R.drawable.emoji_1f442); sEmojisMap.append(0x1f443, R.drawable.emoji_1f443); sEmojisMap.append(0x1f444, R.drawable.emoji_1f444); sEmojisMap.append(0x1f445, R.drawable.emoji_1f445); sEmojisMap.append(0x1f446, R.drawable.emoji_1f446); sEmojisMap.append(0x1f447, R.drawable.emoji_1f447); sEmojisMap.append(0x1f448, R.drawable.emoji_1f448); sEmojisMap.append(0x1f449, R.drawable.emoji_1f449); sEmojisMap.append(0x1f44a, R.drawable.emoji_1f44a); sEmojisMap.append(0x1f44b, R.drawable.emoji_1f44b); sEmojisMap.append(0x1f44c, R.drawable.emoji_1f44c); sEmojisMap.append(0x1f44d, R.drawable.emoji_1f44d); sEmojisMap.append(0x1f44e, R.drawable.emoji_1f44e); sEmojisMap.append(0x1f44f, R.drawable.emoji_1f44f); sEmojisMap.append(0x1f450, R.drawable.emoji_1f450); sEmojisMap.append(0x1f451, R.drawable.emoji_1f451); sEmojisMap.append(0x1f452, R.drawable.emoji_1f452); sEmojisMap.append(0x1f453, R.drawable.emoji_1f453); sEmojisMap.append(0x1f454, R.drawable.emoji_1f454); sEmojisMap.append(0x1f455, R.drawable.emoji_1f455); sEmojisMap.append(0x1f456, R.drawable.emoji_1f456); sEmojisMap.append(0x1f457, R.drawable.emoji_1f457); sEmojisMap.append(0x1f458, R.drawable.emoji_1f458); sEmojisMap.append(0x1f459, R.drawable.emoji_1f459); sEmojisMap.append(0x1f45a, R.drawable.emoji_1f45a); sEmojisMap.append(0x1f45b, R.drawable.emoji_1f45b); sEmojisMap.append(0x1f45c, R.drawable.emoji_1f45c); sEmojisMap.append(0x1f45d, R.drawable.emoji_1f45d); sEmojisMap.append(0x1f45e, R.drawable.emoji_1f45e); sEmojisMap.append(0x1f45f, R.drawable.emoji_1f45f); sEmojisMap.append(0x1f460, R.drawable.emoji_1f460); sEmojisMap.append(0x1f461, R.drawable.emoji_1f461); sEmojisMap.append(0x1f462, R.drawable.emoji_1f462); sEmojisMap.append(0x1f463, R.drawable.emoji_1f463); sEmojisMap.append(0x1f464, R.drawable.emoji_1f464); sEmojisMap.append(0x1f465, R.drawable.emoji_1f465); sEmojisMap.append(0x1f466, R.drawable.emoji_1f466); sEmojisMap.append(0x1f467, R.drawable.emoji_1f467); sEmojisMap.append(0x1f468, R.drawable.emoji_1f468); sEmojisMap.append(0x1f469, R.drawable.emoji_1f469); sEmojisMap.append(0x1f46a, R.drawable.emoji_1f46a); sEmojisMap.append(0x1f46b, R.drawable.emoji_1f46b); sEmojisMap.append(0x1f46c, R.drawable.emoji_1f46c); sEmojisMap.append(0x1f46d, R.drawable.emoji_1f46d); sEmojisMap.append(0x1f46e, R.drawable.emoji_1f46e); sEmojisMap.append(0x1f46f, R.drawable.emoji_1f46f); sEmojisMap.append(0x1f470, R.drawable.emoji_1f470); sEmojisMap.append(0x1f471, R.drawable.emoji_1f471); sEmojisMap.append(0x1f472, R.drawable.emoji_1f472); sEmojisMap.append(0x1f473, R.drawable.emoji_1f473); sEmojisMap.append(0x1f474, R.drawable.emoji_1f474); sEmojisMap.append(0x1f475, R.drawable.emoji_1f475); sEmojisMap.append(0x1f476, R.drawable.emoji_1f476); sEmojisMap.append(0x1f477, R.drawable.emoji_1f477); sEmojisMap.append(0x1f478, R.drawable.emoji_1f478); sEmojisMap.append(0x1f479, R.drawable.emoji_1f479); sEmojisMap.append(0x1f47a, R.drawable.emoji_1f47a); sEmojisMap.append(0x1f47b, R.drawable.emoji_1f47b); sEmojisMap.append(0x1f47c, R.drawable.emoji_1f47c); sEmojisMap.append(0x1f47d, R.drawable.emoji_1f47d); sEmojisMap.append(0x1f47e, R.drawable.emoji_1f47e); sEmojisMap.append(0x1f47f, R.drawable.emoji_1f47f); sEmojisMap.append(0x1f480, R.drawable.emoji_1f480); sEmojisMap.append(0x1f481, R.drawable.emoji_1f481); sEmojisMap.append(0x1f482, R.drawable.emoji_1f482); sEmojisMap.append(0x1f483, R.drawable.emoji_1f483); sEmojisMap.append(0x1f484, R.drawable.emoji_1f484); sEmojisMap.append(0x1f485, R.drawable.emoji_1f485); sEmojisMap.append(0x1f486, R.drawable.emoji_1f486); sEmojisMap.append(0x1f487, R.drawable.emoji_1f487); sEmojisMap.append(0x1f488, R.drawable.emoji_1f488); sEmojisMap.append(0x1f489, R.drawable.emoji_1f489); sEmojisMap.append(0x1f48a, R.drawable.emoji_1f48a); sEmojisMap.append(0x1f48b, R.drawable.emoji_1f48b); sEmojisMap.append(0x1f48c, R.drawable.emoji_1f48c); sEmojisMap.append(0x1f48d, R.drawable.emoji_1f48d); sEmojisMap.append(0x1f48e, R.drawable.emoji_1f48e); sEmojisMap.append(0x1f48f, R.drawable.emoji_1f48f); sEmojisMap.append(0x1f490, R.drawable.emoji_1f490); sEmojisMap.append(0x1f491, R.drawable.emoji_1f491); sEmojisMap.append(0x1f492, R.drawable.emoji_1f492); sEmojisMap.append(0x1f493, R.drawable.emoji_1f493); sEmojisMap.append(0x1f494, R.drawable.emoji_1f494); sEmojisMap.append(0x1f495, R.drawable.emoji_1f495); sEmojisMap.append(0x1f496, R.drawable.emoji_1f496); sEmojisMap.append(0x1f497, R.drawable.emoji_1f497); sEmojisMap.append(0x1f498, R.drawable.emoji_1f498); sEmojisMap.append(0x1f499, R.drawable.emoji_1f499); sEmojisMap.append(0x1f49a, R.drawable.emoji_1f49a); sEmojisMap.append(0x1f49b, R.drawable.emoji_1f49b); sEmojisMap.append(0x1f49c, R.drawable.emoji_1f49c); sEmojisMap.append(0x1f49d, R.drawable.emoji_1f49d); sEmojisMap.append(0x1f49e, R.drawable.emoji_1f49e); sEmojisMap.append(0x1f49f, R.drawable.emoji_1f49f); sEmojisMap.append(0x1f4a0, R.drawable.emoji_1f4a0); sEmojisMap.append(0x1f4a1, R.drawable.emoji_1f4a1); sEmojisMap.append(0x1f4a2, R.drawable.emoji_1f4a2); sEmojisMap.append(0x1f4a3, R.drawable.emoji_1f4a3); sEmojisMap.append(0x1f4a4, R.drawable.emoji_1f4a4); sEmojisMap.append(0x1f4a5, R.drawable.emoji_1f4a5); sEmojisMap.append(0x1f4a6, R.drawable.emoji_1f4a6); sEmojisMap.append(0x1f4a7, R.drawable.emoji_1f4a7); sEmojisMap.append(0x1f4a8, R.drawable.emoji_1f4a8); sEmojisMap.append(0x1f4a9, R.drawable.emoji_1f4a9); sEmojisMap.append(0x1f4aa, R.drawable.emoji_1f4aa); sEmojisMap.append(0x1f4ab, R.drawable.emoji_1f4ab); sEmojisMap.append(0x1f4ac, R.drawable.emoji_1f4ac); sEmojisMap.append(0x1f4ad, R.drawable.emoji_1f4ad); sEmojisMap.append(0x1f4ae, R.drawable.emoji_1f4ae); sEmojisMap.append(0x1f4af, R.drawable.emoji_1f4af); sEmojisMap.append(0x1f4b0, R.drawable.emoji_1f4b0); sEmojisMap.append(0x1f4b1, R.drawable.emoji_1f4b1); sEmojisMap.append(0x1f4b2, R.drawable.emoji_1f4b2); sEmojisMap.append(0x1f4b3, R.drawable.emoji_1f4b3); sEmojisMap.append(0x1f4b4, R.drawable.emoji_1f4b4); sEmojisMap.append(0x1f4b5, R.drawable.emoji_1f4b5); sEmojisMap.append(0x1f4b6, R.drawable.emoji_1f4b6); sEmojisMap.append(0x1f4b7, R.drawable.emoji_1f4b7); sEmojisMap.append(0x1f4b8, R.drawable.emoji_1f4b8); sEmojisMap.append(0x1f4b9, R.drawable.emoji_1f4b9); sEmojisMap.append(0x1f4ba, R.drawable.emoji_1f4ba); sEmojisMap.append(0x1f4bb, R.drawable.emoji_1f4bb); sEmojisMap.append(0x1f4bc, R.drawable.emoji_1f4bc); sEmojisMap.append(0x1f4bd, R.drawable.emoji_1f4bd); sEmojisMap.append(0x1f4be, R.drawable.emoji_1f4be); sEmojisMap.append(0x1f4bf, R.drawable.emoji_1f4bf); sEmojisMap.append(0x1f4c0, R.drawable.emoji_1f4c0); sEmojisMap.append(0x1f4c1, R.drawable.emoji_1f4c1); sEmojisMap.append(0x1f4c2, R.drawable.emoji_1f4c2); sEmojisMap.append(0x1f4c3, R.drawable.emoji_1f4c3); sEmojisMap.append(0x1f4c4, R.drawable.emoji_1f4c4); sEmojisMap.append(0x1f4c5, R.drawable.emoji_1f4c5); sEmojisMap.append(0x1f4c6, R.drawable.emoji_1f4c6); sEmojisMap.append(0x1f4c7, R.drawable.emoji_1f4c7); sEmojisMap.append(0x1f4c8, R.drawable.emoji_1f4c8); sEmojisMap.append(0x1f4c9, R.drawable.emoji_1f4c9); sEmojisMap.append(0x1f4ca, R.drawable.emoji_1f4ca); sEmojisMap.append(0x1f4cb, R.drawable.emoji_1f4cb); sEmojisMap.append(0x1f4cc, R.drawable.emoji_1f4cc); sEmojisMap.append(0x1f4cd, R.drawable.emoji_1f4cd); sEmojisMap.append(0x1f4ce, R.drawable.emoji_1f4ce); sEmojisMap.append(0x1f4cf, R.drawable.emoji_1f4cf); sEmojisMap.append(0x1f4d0, R.drawable.emoji_1f4d0); sEmojisMap.append(0x1f4d1, R.drawable.emoji_1f4d1); sEmojisMap.append(0x1f4d2, R.drawable.emoji_1f4d2); sEmojisMap.append(0x1f4d3, R.drawable.emoji_1f4d3); sEmojisMap.append(0x1f4d4, R.drawable.emoji_1f4d4); sEmojisMap.append(0x1f4d5, R.drawable.emoji_1f4d5); sEmojisMap.append(0x1f4d6, R.drawable.emoji_1f4d6); sEmojisMap.append(0x1f4d7, R.drawable.emoji_1f4d7); sEmojisMap.append(0x1f4d8, R.drawable.emoji_1f4d8); sEmojisMap.append(0x1f4d9, R.drawable.emoji_1f4d9); sEmojisMap.append(0x1f4da, R.drawable.emoji_1f4da); sEmojisMap.append(0x1f4db, R.drawable.emoji_1f4db); sEmojisMap.append(0x1f4dc, R.drawable.emoji_1f4dc); sEmojisMap.append(0x1f4dd, R.drawable.emoji_1f4dd); sEmojisMap.append(0x1f4de, R.drawable.emoji_1f4de); sEmojisMap.append(0x1f4df, R.drawable.emoji_1f4df); sEmojisMap.append(0x1f4e0, R.drawable.emoji_1f4e0); sEmojisMap.append(0x1f4e1, R.drawable.emoji_1f4e1); sEmojisMap.append(0x1f4e2, R.drawable.emoji_1f4e2); sEmojisMap.append(0x1f4e3, R.drawable.emoji_1f4e3); sEmojisMap.append(0x1f4e4, R.drawable.emoji_1f4e4); sEmojisMap.append(0x1f4e5, R.drawable.emoji_1f4e5); sEmojisMap.append(0x1f4e6, R.drawable.emoji_1f4e6); sEmojisMap.append(0x1f4e7, R.drawable.emoji_1f4e7); sEmojisMap.append(0x1f4e8, R.drawable.emoji_1f4e8); sEmojisMap.append(0x1f4e9, R.drawable.emoji_1f4e9); sEmojisMap.append(0x1f4ea, R.drawable.emoji_1f4ea); sEmojisMap.append(0x1f4eb, R.drawable.emoji_1f4eb); sEmojisMap.append(0x1f4ec, R.drawable.emoji_1f4ec); sEmojisMap.append(0x1f4ed, R.drawable.emoji_1f4ed); sEmojisMap.append(0x1f4ee, R.drawable.emoji_1f4ee); sEmojisMap.append(0x1f4ef, R.drawable.emoji_1f4ef); sEmojisMap.append(0x1f4f0, R.drawable.emoji_1f4f0); sEmojisMap.append(0x1f4f1, R.drawable.emoji_1f4f1); sEmojisMap.append(0x1f4f2, R.drawable.emoji_1f4f2); sEmojisMap.append(0x1f4f3, R.drawable.emoji_1f4f3); sEmojisMap.append(0x1f4f4, R.drawable.emoji_1f4f4); sEmojisMap.append(0x1f4f5, R.drawable.emoji_1f4f5); sEmojisMap.append(0x1f4f6, R.drawable.emoji_1f4f6); sEmojisMap.append(0x1f4f7, R.drawable.emoji_1f4f7); sEmojisMap.append(0x1f4f9, R.drawable.emoji_1f4f9); sEmojisMap.append(0x1f4fa, R.drawable.emoji_1f4fa); sEmojisMap.append(0x1f4fb, R.drawable.emoji_1f4fb); sEmojisMap.append(0x1f4fc, R.drawable.emoji_1f4fc); sEmojisMap.append(0x1f500, R.drawable.emoji_1f500); sEmojisMap.append(0x1f501, R.drawable.emoji_1f501); sEmojisMap.append(0x1f502, R.drawable.emoji_1f502); sEmojisMap.append(0x1f503, R.drawable.emoji_1f503); sEmojisMap.append(0x1f504, R.drawable.emoji_1f504); sEmojisMap.append(0x1f505, R.drawable.emoji_1f505); sEmojisMap.append(0x1f506, R.drawable.emoji_1f506); sEmojisMap.append(0x1f507, R.drawable.emoji_1f507); sEmojisMap.append(0x1f508, R.drawable.emoji_1f508); sEmojisMap.append(0x1f509, R.drawable.emoji_1f509); sEmojisMap.append(0x1f50a, R.drawable.emoji_1f50a); sEmojisMap.append(0x1f50b, R.drawable.emoji_1f50b); sEmojisMap.append(0x1f50c, R.drawable.emoji_1f50c); sEmojisMap.append(0x1f50d, R.drawable.emoji_1f50d); sEmojisMap.append(0x1f50e, R.drawable.emoji_1f50e); sEmojisMap.append(0x1f50f, R.drawable.emoji_1f50f); sEmojisMap.append(0x1f510, R.drawable.emoji_1f510); sEmojisMap.append(0x1f511, R.drawable.emoji_1f511); sEmojisMap.append(0x1f512, R.drawable.emoji_1f512); sEmojisMap.append(0x1f513, R.drawable.emoji_1f513); sEmojisMap.append(0x1f514, R.drawable.emoji_1f514); sEmojisMap.append(0x1f515, R.drawable.emoji_1f515); sEmojisMap.append(0x1f516, R.drawable.emoji_1f516); sEmojisMap.append(0x1f517, R.drawable.emoji_1f517); sEmojisMap.append(0x1f518, R.drawable.emoji_1f518); sEmojisMap.append(0x1f519, R.drawable.emoji_1f519); sEmojisMap.append(0x1f51a, R.drawable.emoji_1f51a); sEmojisMap.append(0x1f51b, R.drawable.emoji_1f51b); sEmojisMap.append(0x1f51c, R.drawable.emoji_1f51c); sEmojisMap.append(0x1f51d, R.drawable.emoji_1f51d); sEmojisMap.append(0x1f51e, R.drawable.emoji_1f51e); sEmojisMap.append(0x1f51f, R.drawable.emoji_1f51f); sEmojisMap.append(0x1f520, R.drawable.emoji_1f520); sEmojisMap.append(0x1f521, R.drawable.emoji_1f521); sEmojisMap.append(0x1f522, R.drawable.emoji_1f522); sEmojisMap.append(0x1f523, R.drawable.emoji_1f523); sEmojisMap.append(0x1f524, R.drawable.emoji_1f524); sEmojisMap.append(0x1f525, R.drawable.emoji_1f525); sEmojisMap.append(0x1f526, R.drawable.emoji_1f526); sEmojisMap.append(0x1f527, R.drawable.emoji_1f527); sEmojisMap.append(0x1f528, R.drawable.emoji_1f528); sEmojisMap.append(0x1f529, R.drawable.emoji_1f529); sEmojisMap.append(0x1f52a, R.drawable.emoji_1f52a); sEmojisMap.append(0x1f52b, R.drawable.emoji_1f52b); sEmojisMap.append(0x1f52c, R.drawable.emoji_1f52c); sEmojisMap.append(0x1f52d, R.drawable.emoji_1f52d); sEmojisMap.append(0x1f52e, R.drawable.emoji_1f52e); sEmojisMap.append(0x1f52f, R.drawable.emoji_1f52f); sEmojisMap.append(0x1f530, R.drawable.emoji_1f530); sEmojisMap.append(0x1f531, R.drawable.emoji_1f531); sEmojisMap.append(0x1f532, R.drawable.emoji_1f532); sEmojisMap.append(0x1f533, R.drawable.emoji_1f533); sEmojisMap.append(0x1f534, R.drawable.emoji_1f534); sEmojisMap.append(0x1f535, R.drawable.emoji_1f535); sEmojisMap.append(0x1f536, R.drawable.emoji_1f536); sEmojisMap.append(0x1f537, R.drawable.emoji_1f537); sEmojisMap.append(0x1f538, R.drawable.emoji_1f538); sEmojisMap.append(0x1f539, R.drawable.emoji_1f539); sEmojisMap.append(0x1f53a, R.drawable.emoji_1f53a); sEmojisMap.append(0x1f53b, R.drawable.emoji_1f53b); sEmojisMap.append(0x1f53c, R.drawable.emoji_1f53c); sEmojisMap.append(0x1f53d, R.drawable.emoji_1f53d); sEmojisMap.append(0x1f550, R.drawable.emoji_1f550); sEmojisMap.append(0x1f551, R.drawable.emoji_1f551); sEmojisMap.append(0x1f552, R.drawable.emoji_1f552); sEmojisMap.append(0x1f553, R.drawable.emoji_1f553); sEmojisMap.append(0x1f554, R.drawable.emoji_1f554); sEmojisMap.append(0x1f555, R.drawable.emoji_1f555); sEmojisMap.append(0x1f556, R.drawable.emoji_1f556); sEmojisMap.append(0x1f557, R.drawable.emoji_1f557); sEmojisMap.append(0x1f558, R.drawable.emoji_1f558); sEmojisMap.append(0x1f559, R.drawable.emoji_1f559); sEmojisMap.append(0x1f55a, R.drawable.emoji_1f55a); sEmojisMap.append(0x1f55b, R.drawable.emoji_1f55b); sEmojisMap.append(0x1f55c, R.drawable.emoji_1f55c); sEmojisMap.append(0x1f55d, R.drawable.emoji_1f55d); sEmojisMap.append(0x1f55e, R.drawable.emoji_1f55e); sEmojisMap.append(0x1f55f, R.drawable.emoji_1f55f); sEmojisMap.append(0x1f560, R.drawable.emoji_1f560); sEmojisMap.append(0x1f561, R.drawable.emoji_1f561); sEmojisMap.append(0x1f562, R.drawable.emoji_1f562); sEmojisMap.append(0x1f563, R.drawable.emoji_1f563); sEmojisMap.append(0x1f564, R.drawable.emoji_1f564); sEmojisMap.append(0x1f565, R.drawable.emoji_1f565); sEmojisMap.append(0x1f566, R.drawable.emoji_1f566); sEmojisMap.append(0x1f567, R.drawable.emoji_1f567); sEmojisMap.append(0x1f5fb, R.drawable.emoji_1f5fb); sEmojisMap.append(0x1f5fc, R.drawable.emoji_1f5fc); sEmojisMap.append(0x1f5fd, R.drawable.emoji_1f5fd); sEmojisMap.append(0x1f5fe, R.drawable.emoji_1f5fe); sEmojisMap.append(0x1f5ff, R.drawable.emoji_1f5ff); sEmojisMap.append(0x1f600, R.drawable.emoji_1f600); sEmojisMap.append(0x1f601, R.drawable.emoji_1f601); sEmojisMap.append(0x1f602, R.drawable.emoji_1f602); sEmojisMap.append(0x1f603, R.drawable.emoji_1f603); sEmojisMap.append(0x1f604, R.drawable.emoji_1f604); sEmojisMap.append(0x1f605, R.drawable.emoji_1f605); sEmojisMap.append(0x1f606, R.drawable.emoji_1f606); sEmojisMap.append(0x1f607, R.drawable.emoji_1f607); sEmojisMap.append(0x1f608, R.drawable.emoji_1f608); sEmojisMap.append(0x1f609, R.drawable.emoji_1f609); sEmojisMap.append(0x1f60a, R.drawable.emoji_1f60a); sEmojisMap.append(0x1f60b, R.drawable.emoji_1f60b); sEmojisMap.append(0x1f60c, R.drawable.emoji_1f60c); sEmojisMap.append(0x1f60d, R.drawable.emoji_1f60d); sEmojisMap.append(0x1f60e, R.drawable.emoji_1f60e); sEmojisMap.append(0x1f60f, R.drawable.emoji_1f60f); sEmojisMap.append(0x1f610, R.drawable.emoji_1f610); sEmojisMap.append(0x1f611, R.drawable.emoji_1f611); sEmojisMap.append(0x1f612, R.drawable.emoji_1f612); sEmojisMap.append(0x1f613, R.drawable.emoji_1f613); sEmojisMap.append(0x1f614, R.drawable.emoji_1f614); sEmojisMap.append(0x1f615, R.drawable.emoji_1f615); sEmojisMap.append(0x1f616, R.drawable.emoji_1f616); sEmojisMap.append(0x1f617, R.drawable.emoji_1f617); sEmojisMap.append(0x1f618, R.drawable.emoji_1f618); sEmojisMap.append(0x1f619, R.drawable.emoji_1f619); sEmojisMap.append(0x1f61a, R.drawable.emoji_1f61a); sEmojisMap.append(0x1f61b, R.drawable.emoji_1f61b); sEmojisMap.append(0x1f61c, R.drawable.emoji_1f61c); sEmojisMap.append(0x1f61d, R.drawable.emoji_1f61d); sEmojisMap.append(0x1f61e, R.drawable.emoji_1f61e); sEmojisMap.append(0x1f61f, R.drawable.emoji_1f61f); sEmojisMap.append(0x1f620, R.drawable.emoji_1f620); sEmojisMap.append(0x1f621, R.drawable.emoji_1f621); sEmojisMap.append(0x1f622, R.drawable.emoji_1f622); sEmojisMap.append(0x1f623, R.drawable.emoji_1f623); sEmojisMap.append(0x1f624, R.drawable.emoji_1f624); sEmojisMap.append(0x1f625, R.drawable.emoji_1f625); sEmojisMap.append(0x1f626, R.drawable.emoji_1f626); sEmojisMap.append(0x1f627, R.drawable.emoji_1f627); sEmojisMap.append(0x1f628, R.drawable.emoji_1f628); sEmojisMap.append(0x1f629, R.drawable.emoji_1f629); sEmojisMap.append(0x1f62a, R.drawable.emoji_1f62a); sEmojisMap.append(0x1f62b, R.drawable.emoji_1f62b); sEmojisMap.append(0x1f62c, R.drawable.emoji_1f62c); sEmojisMap.append(0x1f62d, R.drawable.emoji_1f62d); sEmojisMap.append(0x1f62e, R.drawable.emoji_1f62e); sEmojisMap.append(0x1f62f, R.drawable.emoji_1f62f); sEmojisMap.append(0x1f630, R.drawable.emoji_1f630); sEmojisMap.append(0x1f631, R.drawable.emoji_1f631); sEmojisMap.append(0x1f632, R.drawable.emoji_1f632); sEmojisMap.append(0x1f633, R.drawable.emoji_1f633); sEmojisMap.append(0x1f634, R.drawable.emoji_1f634); sEmojisMap.append(0x1f635, R.drawable.emoji_1f635); sEmojisMap.append(0x1f636, R.drawable.emoji_1f636); sEmojisMap.append(0x1f637, R.drawable.emoji_1f637); sEmojisMap.append(0x1f638, R.drawable.emoji_1f638); sEmojisMap.append(0x1f639, R.drawable.emoji_1f639); sEmojisMap.append(0x1f63a, R.drawable.emoji_1f63a); sEmojisMap.append(0x1f63b, R.drawable.emoji_1f63b); sEmojisMap.append(0x1f63c, R.drawable.emoji_1f63c); sEmojisMap.append(0x1f63d, R.drawable.emoji_1f63d); sEmojisMap.append(0x1f63e, R.drawable.emoji_1f63e); sEmojisMap.append(0x1f63f, R.drawable.emoji_1f63f); sEmojisMap.append(0x1f640, R.drawable.emoji_1f640); sEmojisMap.append(0x1f645, R.drawable.emoji_1f645); sEmojisMap.append(0x1f646, R.drawable.emoji_1f646); sEmojisMap.append(0x1f647, R.drawable.emoji_1f647); sEmojisMap.append(0x1f648, R.drawable.emoji_1f648); sEmojisMap.append(0x1f649, R.drawable.emoji_1f649); sEmojisMap.append(0x1f64a, R.drawable.emoji_1f64a); sEmojisMap.append(0x1f64b, R.drawable.emoji_1f64b); sEmojisMap.append(0x1f64c, R.drawable.emoji_1f64c); sEmojisMap.append(0x1f64d, R.drawable.emoji_1f64d); sEmojisMap.append(0x1f64e, R.drawable.emoji_1f64e); sEmojisMap.append(0x1f64f, R.drawable.emoji_1f64f); sEmojisMap.append(0x1f680, R.drawable.emoji_1f680); sEmojisMap.append(0x1f681, R.drawable.emoji_1f681); sEmojisMap.append(0x1f682, R.drawable.emoji_1f682); sEmojisMap.append(0x1f683, R.drawable.emoji_1f683); sEmojisMap.append(0x1f684, R.drawable.emoji_1f684); sEmojisMap.append(0x1f685, R.drawable.emoji_1f685); sEmojisMap.append(0x1f686, R.drawable.emoji_1f686); sEmojisMap.append(0x1f687, R.drawable.emoji_1f687); sEmojisMap.append(0x1f688, R.drawable.emoji_1f688); sEmojisMap.append(0x1f689, R.drawable.emoji_1f689); sEmojisMap.append(0x1f68a, R.drawable.emoji_1f68a); sEmojisMap.append(0x1f68b, R.drawable.emoji_1f68b); sEmojisMap.append(0x1f68c, R.drawable.emoji_1f68c); sEmojisMap.append(0x1f68d, R.drawable.emoji_1f68d); sEmojisMap.append(0x1f68e, R.drawable.emoji_1f68e); sEmojisMap.append(0x1f68f, R.drawable.emoji_1f68f); sEmojisMap.append(0x1f690, R.drawable.emoji_1f690); sEmojisMap.append(0x1f691, R.drawable.emoji_1f691); sEmojisMap.append(0x1f692, R.drawable.emoji_1f692); sEmojisMap.append(0x1f693, R.drawable.emoji_1f693); sEmojisMap.append(0x1f694, R.drawable.emoji_1f694); sEmojisMap.append(0x1f695, R.drawable.emoji_1f695); sEmojisMap.append(0x1f696, R.drawable.emoji_1f696); sEmojisMap.append(0x1f697, R.drawable.emoji_1f697); sEmojisMap.append(0x1f698, R.drawable.emoji_1f698); sEmojisMap.append(0x1f699, R.drawable.emoji_1f699); sEmojisMap.append(0x1f69a, R.drawable.emoji_1f69a); sEmojisMap.append(0x1f69b, R.drawable.emoji_1f69b); sEmojisMap.append(0x1f69c, R.drawable.emoji_1f69c); sEmojisMap.append(0x1f69d, R.drawable.emoji_1f69d); sEmojisMap.append(0x1f69e, R.drawable.emoji_1f69e); sEmojisMap.append(0x1f69f, R.drawable.emoji_1f69f); sEmojisMap.append(0x1f6a0, R.drawable.emoji_1f6a0); sEmojisMap.append(0x1f6a1, R.drawable.emoji_1f6a1); sEmojisMap.append(0x1f6a2, R.drawable.emoji_1f6a2); sEmojisMap.append(0x1f6a3, R.drawable.emoji_1f6a3); sEmojisMap.append(0x1f6a4, R.drawable.emoji_1f6a4); sEmojisMap.append(0x1f6a5, R.drawable.emoji_1f6a5); sEmojisMap.append(0x1f6a6, R.drawable.emoji_1f6a6); sEmojisMap.append(0x1f6a7, R.drawable.emoji_1f6a7); sEmojisMap.append(0x1f6a8, R.drawable.emoji_1f6a8); sEmojisMap.append(0x1f6a9, R.drawable.emoji_1f6a9); sEmojisMap.append(0x1f6aa, R.drawable.emoji_1f6aa); sEmojisMap.append(0x1f6ab, R.drawable.emoji_1f6ab); sEmojisMap.append(0x1f6ac, R.drawable.emoji_1f6ac); sEmojisMap.append(0x1f6ad, R.drawable.emoji_1f6ad); sEmojisMap.append(0x1f6ae, R.drawable.emoji_1f6ae); sEmojisMap.append(0x1f6af, R.drawable.emoji_1f6af); sEmojisMap.append(0x1f6b0, R.drawable.emoji_1f6b0); sEmojisMap.append(0x1f6b1, R.drawable.emoji_1f6b1); sEmojisMap.append(0x1f6b2, R.drawable.emoji_1f6b2); sEmojisMap.append(0x1f6b3, R.drawable.emoji_1f6b3); sEmojisMap.append(0x1f6b4, R.drawable.emoji_1f6b4); sEmojisMap.append(0x1f6b5, R.drawable.emoji_1f6b5); sEmojisMap.append(0x1f6b6, R.drawable.emoji_1f6b6); sEmojisMap.append(0x1f6b7, R.drawable.emoji_1f6b7); sEmojisMap.append(0x1f6b8, R.drawable.emoji_1f6b8); sEmojisMap.append(0x1f6b9, R.drawable.emoji_1f6b9); sEmojisMap.append(0x1f6ba, R.drawable.emoji_1f6ba); sEmojisMap.append(0x1f6bb, R.drawable.emoji_1f6bb); sEmojisMap.append(0x1f6bc, R.drawable.emoji_1f6bc); sEmojisMap.append(0x1f6bd, R.drawable.emoji_1f6bd); sEmojisMap.append(0x1f6be, R.drawable.emoji_1f6be); sEmojisMap.append(0x1f6bf, R.drawable.emoji_1f6bf); sEmojisMap.append(0x1f6c0, R.drawable.emoji_1f6c0); sEmojisMap.append(0x1f6c1, R.drawable.emoji_1f6c1); sEmojisMap.append(0x1f6c2, R.drawable.emoji_1f6c2); sEmojisMap.append(0x1f6c3, R.drawable.emoji_1f6c3); sEmojisMap.append(0x1f6c4, R.drawable.emoji_1f6c4); sEmojisMap.append(0x1f6c5, R.drawable.emoji_1f6c5); sSoftbanksMap.append(0xe001, R.drawable.emoji_1f466); sSoftbanksMap.append(0xe002, R.drawable.emoji_1f467); sSoftbanksMap.append(0xe003, R.drawable.emoji_1f48b); sSoftbanksMap.append(0xe004, R.drawable.emoji_1f468); sSoftbanksMap.append(0xe005, R.drawable.emoji_1f469); sSoftbanksMap.append(0xe006, R.drawable.emoji_1f455); sSoftbanksMap.append(0xe007, R.drawable.emoji_1f45e); sSoftbanksMap.append(0xe008, R.drawable.emoji_1f4f7); sSoftbanksMap.append(0xe009, R.drawable.emoji_1f4de); sSoftbanksMap.append(0xe00a, R.drawable.emoji_1f4f1); sSoftbanksMap.append(0xe00b, R.drawable.emoji_1f4e0); sSoftbanksMap.append(0xe00c, R.drawable.emoji_1f4bb); sSoftbanksMap.append(0xe00d, R.drawable.emoji_1f44a); sSoftbanksMap.append(0xe00e, R.drawable.emoji_1f44d); sSoftbanksMap.append(0xe00f, R.drawable.emoji_261d); sSoftbanksMap.append(0xe010, R.drawable.emoji_270a); sSoftbanksMap.append(0xe011, R.drawable.emoji_270c); sSoftbanksMap.append(0xe012, R.drawable.emoji_1f64b); sSoftbanksMap.append(0xe013, R.drawable.emoji_1f3bf); sSoftbanksMap.append(0xe014, R.drawable.emoji_26f3); sSoftbanksMap.append(0xe015, R.drawable.emoji_1f3be); sSoftbanksMap.append(0xe016, R.drawable.emoji_26be); sSoftbanksMap.append(0xe017, R.drawable.emoji_1f3c4); sSoftbanksMap.append(0xe018, R.drawable.emoji_26bd); sSoftbanksMap.append(0xe019, R.drawable.emoji_1f3a3); sSoftbanksMap.append(0xe01a, R.drawable.emoji_1f434); sSoftbanksMap.append(0xe01b, R.drawable.emoji_1f697); sSoftbanksMap.append(0xe01c, R.drawable.emoji_26f5); sSoftbanksMap.append(0xe01d, R.drawable.emoji_2708); sSoftbanksMap.append(0xe01e, R.drawable.emoji_1f683); sSoftbanksMap.append(0xe01f, R.drawable.emoji_1f685); sSoftbanksMap.append(0xe020, R.drawable.emoji_2753); sSoftbanksMap.append(0xe021, R.drawable.emoji_2757); sSoftbanksMap.append(0xe022, R.drawable.emoji_2764); sSoftbanksMap.append(0xe023, R.drawable.emoji_1f494); sSoftbanksMap.append(0xe024, R.drawable.emoji_1f550); sSoftbanksMap.append(0xe025, R.drawable.emoji_1f551); sSoftbanksMap.append(0xe026, R.drawable.emoji_1f552); sSoftbanksMap.append(0xe027, R.drawable.emoji_1f553); sSoftbanksMap.append(0xe028, R.drawable.emoji_1f554); sSoftbanksMap.append(0xe029, R.drawable.emoji_1f555); sSoftbanksMap.append(0xe02a, R.drawable.emoji_1f556); sSoftbanksMap.append(0xe02b, R.drawable.emoji_1f557); sSoftbanksMap.append(0xe02c, R.drawable.emoji_1f558); sSoftbanksMap.append(0xe02d, R.drawable.emoji_1f559); sSoftbanksMap.append(0xe02e, R.drawable.emoji_1f55a); sSoftbanksMap.append(0xe02f, R.drawable.emoji_1f55b); sSoftbanksMap.append(0xe030, R.drawable.emoji_1f338); sSoftbanksMap.append(0xe031, R.drawable.emoji_1f531); sSoftbanksMap.append(0xe032, R.drawable.emoji_1f339); sSoftbanksMap.append(0xe033, R.drawable.emoji_1f384); sSoftbanksMap.append(0xe034, R.drawable.emoji_1f48d); sSoftbanksMap.append(0xe035, R.drawable.emoji_1f48e); sSoftbanksMap.append(0xe036, R.drawable.emoji_1f3e0); sSoftbanksMap.append(0xe037, R.drawable.emoji_26ea); sSoftbanksMap.append(0xe038, R.drawable.emoji_1f3e2); sSoftbanksMap.append(0xe039, R.drawable.emoji_1f689); sSoftbanksMap.append(0xe03a, R.drawable.emoji_26fd); sSoftbanksMap.append(0xe03b, R.drawable.emoji_1f5fb); sSoftbanksMap.append(0xe03c, R.drawable.emoji_1f3a4); sSoftbanksMap.append(0xe03d, R.drawable.emoji_1f3a5); sSoftbanksMap.append(0xe03e, R.drawable.emoji_1f3b5); sSoftbanksMap.append(0xe03f, R.drawable.emoji_1f511); sSoftbanksMap.append(0xe040, R.drawable.emoji_1f3b7); sSoftbanksMap.append(0xe041, R.drawable.emoji_1f3b8); sSoftbanksMap.append(0xe042, R.drawable.emoji_1f3ba); sSoftbanksMap.append(0xe043, R.drawable.emoji_1f374); sSoftbanksMap.append(0xe044, R.drawable.emoji_1f377); sSoftbanksMap.append(0xe045, R.drawable.emoji_2615); sSoftbanksMap.append(0xe046, R.drawable.emoji_1f370); sSoftbanksMap.append(0xe047, R.drawable.emoji_1f37a); sSoftbanksMap.append(0xe048, R.drawable.emoji_26c4); sSoftbanksMap.append(0xe049, R.drawable.emoji_2601); sSoftbanksMap.append(0xe04a, R.drawable.emoji_2600); sSoftbanksMap.append(0xe04b, R.drawable.emoji_2614); sSoftbanksMap.append(0xe04c, R.drawable.emoji_1f313); sSoftbanksMap.append(0xe04d, R.drawable.emoji_1f304); sSoftbanksMap.append(0xe04e, R.drawable.emoji_1f47c); sSoftbanksMap.append(0xe04f, R.drawable.emoji_1f431); sSoftbanksMap.append(0xe050, R.drawable.emoji_1f42f); sSoftbanksMap.append(0xe051, R.drawable.emoji_1f43b); sSoftbanksMap.append(0xe052, R.drawable.emoji_1f429); sSoftbanksMap.append(0xe053, R.drawable.emoji_1f42d); sSoftbanksMap.append(0xe054, R.drawable.emoji_1f433); sSoftbanksMap.append(0xe055, R.drawable.emoji_1f427); sSoftbanksMap.append(0xe056, R.drawable.emoji_1f60a); sSoftbanksMap.append(0xe057, R.drawable.emoji_1f603); sSoftbanksMap.append(0xe058, R.drawable.emoji_1f61e); sSoftbanksMap.append(0xe059, R.drawable.emoji_1f620); sSoftbanksMap.append(0xe05a, R.drawable.emoji_1f4a9); sSoftbanksMap.append(0xe101, R.drawable.emoji_1f4ea); sSoftbanksMap.append(0xe102, R.drawable.emoji_1f4ee); sSoftbanksMap.append(0xe103, R.drawable.emoji_1f4e7); sSoftbanksMap.append(0xe104, R.drawable.emoji_1f4f2); sSoftbanksMap.append(0xe105, R.drawable.emoji_1f61c); sSoftbanksMap.append(0xe106, R.drawable.emoji_1f60d); sSoftbanksMap.append(0xe107, R.drawable.emoji_1f631); sSoftbanksMap.append(0xe108, R.drawable.emoji_1f613); sSoftbanksMap.append(0xe109, R.drawable.emoji_1f435); sSoftbanksMap.append(0xe10a, R.drawable.emoji_1f419); sSoftbanksMap.append(0xe10b, R.drawable.emoji_1f437); sSoftbanksMap.append(0xe10c, R.drawable.emoji_1f47d); sSoftbanksMap.append(0xe10d, R.drawable.emoji_1f680); sSoftbanksMap.append(0xe10e, R.drawable.emoji_1f451); sSoftbanksMap.append(0xe10f, R.drawable.emoji_1f4a1); sSoftbanksMap.append(0xe110, R.drawable.emoji_1f331); sSoftbanksMap.append(0xe111, R.drawable.emoji_1f48f); sSoftbanksMap.append(0xe112, R.drawable.emoji_1f381); sSoftbanksMap.append(0xe113, R.drawable.emoji_1f52b); sSoftbanksMap.append(0xe114, R.drawable.emoji_1f50d); sSoftbanksMap.append(0xe115, R.drawable.emoji_1f3c3); sSoftbanksMap.append(0xe116, R.drawable.emoji_1f528); sSoftbanksMap.append(0xe117, R.drawable.emoji_1f386); sSoftbanksMap.append(0xe118, R.drawable.emoji_1f341); sSoftbanksMap.append(0xe119, R.drawable.emoji_1f342); sSoftbanksMap.append(0xe11a, R.drawable.emoji_1f47f); sSoftbanksMap.append(0xe11b, R.drawable.emoji_1f47b); sSoftbanksMap.append(0xe11c, R.drawable.emoji_1f480); sSoftbanksMap.append(0xe11d, R.drawable.emoji_1f525); sSoftbanksMap.append(0xe11e, R.drawable.emoji_1f4bc); sSoftbanksMap.append(0xe11f, R.drawable.emoji_1f4ba); sSoftbanksMap.append(0xe120, R.drawable.emoji_1f354); sSoftbanksMap.append(0xe121, R.drawable.emoji_26f2); sSoftbanksMap.append(0xe122, R.drawable.emoji_26fa); sSoftbanksMap.append(0xe123, R.drawable.emoji_2668); sSoftbanksMap.append(0xe124, R.drawable.emoji_1f3a1); sSoftbanksMap.append(0xe125, R.drawable.emoji_1f3ab); sSoftbanksMap.append(0xe126, R.drawable.emoji_1f4bf); sSoftbanksMap.append(0xe127, R.drawable.emoji_1f4c0); sSoftbanksMap.append(0xe128, R.drawable.emoji_1f4fb); sSoftbanksMap.append(0xe129, R.drawable.emoji_1f4fc); sSoftbanksMap.append(0xe12a, R.drawable.emoji_1f4fa); sSoftbanksMap.append(0xe12b, R.drawable.emoji_1f47e); sSoftbanksMap.append(0xe12c, R.drawable.emoji_303d); sSoftbanksMap.append(0xe12d, R.drawable.emoji_1f004); sSoftbanksMap.append(0xe12e, R.drawable.emoji_1f19a); sSoftbanksMap.append(0xe12f, R.drawable.emoji_1f4b0); sSoftbanksMap.append(0xe130, R.drawable.emoji_1f3af); sSoftbanksMap.append(0xe131, R.drawable.emoji_1f3c6); sSoftbanksMap.append(0xe132, R.drawable.emoji_1f3c1); sSoftbanksMap.append(0xe133, R.drawable.emoji_1f3b0); sSoftbanksMap.append(0xe134, R.drawable.emoji_1f40e); sSoftbanksMap.append(0xe135, R.drawable.emoji_1f6a4); sSoftbanksMap.append(0xe136, R.drawable.emoji_1f6b2); sSoftbanksMap.append(0xe137, R.drawable.emoji_1f6a7); sSoftbanksMap.append(0xe138, R.drawable.emoji_1f6b9); sSoftbanksMap.append(0xe139, R.drawable.emoji_1f6ba); sSoftbanksMap.append(0xe13a, R.drawable.emoji_1f6bc); sSoftbanksMap.append(0xe13b, R.drawable.emoji_1f489); sSoftbanksMap.append(0xe13c, R.drawable.emoji_1f4a4); sSoftbanksMap.append(0xe13d, R.drawable.emoji_26a1); sSoftbanksMap.append(0xe13e, R.drawable.emoji_1f460); sSoftbanksMap.append(0xe13f, R.drawable.emoji_1f6c0); sSoftbanksMap.append(0xe140, R.drawable.emoji_1f6bd); sSoftbanksMap.append(0xe141, R.drawable.emoji_1f50a); sSoftbanksMap.append(0xe142, R.drawable.emoji_1f4e2); sSoftbanksMap.append(0xe143, R.drawable.emoji_1f38c); sSoftbanksMap.append(0xe144, R.drawable.emoji_1f50f); sSoftbanksMap.append(0xe145, R.drawable.emoji_1f513); sSoftbanksMap.append(0xe146, R.drawable.emoji_1f306); sSoftbanksMap.append(0xe147, R.drawable.emoji_1f373); sSoftbanksMap.append(0xe148, R.drawable.emoji_1f4c7); sSoftbanksMap.append(0xe149, R.drawable.emoji_1f4b1); sSoftbanksMap.append(0xe14a, R.drawable.emoji_1f4b9); sSoftbanksMap.append(0xe14b, R.drawable.emoji_1f4e1); sSoftbanksMap.append(0xe14c, R.drawable.emoji_1f4aa); sSoftbanksMap.append(0xe14d, R.drawable.emoji_1f3e6); sSoftbanksMap.append(0xe14e, R.drawable.emoji_1f6a5); sSoftbanksMap.append(0xe14f, R.drawable.emoji_1f17f); sSoftbanksMap.append(0xe150, R.drawable.emoji_1f68f); sSoftbanksMap.append(0xe151, R.drawable.emoji_1f6bb); sSoftbanksMap.append(0xe152, R.drawable.emoji_1f46e); sSoftbanksMap.append(0xe153, R.drawable.emoji_1f3e3); sSoftbanksMap.append(0xe154, R.drawable.emoji_1f3e7); sSoftbanksMap.append(0xe155, R.drawable.emoji_1f3e5); sSoftbanksMap.append(0xe156, R.drawable.emoji_1f3ea); sSoftbanksMap.append(0xe157, R.drawable.emoji_1f3eb); sSoftbanksMap.append(0xe158, R.drawable.emoji_1f3e8); sSoftbanksMap.append(0xe159, R.drawable.emoji_1f68c); sSoftbanksMap.append(0xe15a, R.drawable.emoji_1f695); sSoftbanksMap.append(0xe201, R.drawable.emoji_1f6b6); sSoftbanksMap.append(0xe202, R.drawable.emoji_1f6a2); sSoftbanksMap.append(0xe203, R.drawable.emoji_1f201); sSoftbanksMap.append(0xe204, R.drawable.emoji_1f49f); sSoftbanksMap.append(0xe205, R.drawable.emoji_2734); sSoftbanksMap.append(0xe206, R.drawable.emoji_2733); sSoftbanksMap.append(0xe207, R.drawable.emoji_1f51e); sSoftbanksMap.append(0xe208, R.drawable.emoji_1f6ad); sSoftbanksMap.append(0xe209, R.drawable.emoji_1f530); sSoftbanksMap.append(0xe20a, R.drawable.emoji_267f); sSoftbanksMap.append(0xe20b, R.drawable.emoji_1f4f6); sSoftbanksMap.append(0xe20c, R.drawable.emoji_2665); sSoftbanksMap.append(0xe20d, R.drawable.emoji_2666); sSoftbanksMap.append(0xe20e, R.drawable.emoji_2660); sSoftbanksMap.append(0xe20f, R.drawable.emoji_2663); sSoftbanksMap.append(0xe210, R.drawable.emoji_0023); sSoftbanksMap.append(0xe211, R.drawable.emoji_27bf); sSoftbanksMap.append(0xe212, R.drawable.emoji_1f195); sSoftbanksMap.append(0xe213, R.drawable.emoji_1f199); sSoftbanksMap.append(0xe214, R.drawable.emoji_1f192); sSoftbanksMap.append(0xe215, R.drawable.emoji_1f236); sSoftbanksMap.append(0xe216, R.drawable.emoji_1f21a); sSoftbanksMap.append(0xe217, R.drawable.emoji_1f237); sSoftbanksMap.append(0xe218, R.drawable.emoji_1f238); sSoftbanksMap.append(0xe219, R.drawable.emoji_1f534); sSoftbanksMap.append(0xe21a, R.drawable.emoji_1f532); sSoftbanksMap.append(0xe21b, R.drawable.emoji_1f533); sSoftbanksMap.append(0xe21c, R.drawable.emoji_0031); sSoftbanksMap.append(0xe21d, R.drawable.emoji_0032); sSoftbanksMap.append(0xe21e, R.drawable.emoji_0033); sSoftbanksMap.append(0xe21f, R.drawable.emoji_0034); sSoftbanksMap.append(0xe220, R.drawable.emoji_0035); sSoftbanksMap.append(0xe221, R.drawable.emoji_0036); sSoftbanksMap.append(0xe222, R.drawable.emoji_0037); sSoftbanksMap.append(0xe223, R.drawable.emoji_0038); sSoftbanksMap.append(0xe224, R.drawable.emoji_0039); sSoftbanksMap.append(0xe225, R.drawable.emoji_0030); sSoftbanksMap.append(0xe226, R.drawable.emoji_1f250); sSoftbanksMap.append(0xe227, R.drawable.emoji_1f239); sSoftbanksMap.append(0xe228, R.drawable.emoji_1f202); sSoftbanksMap.append(0xe229, R.drawable.emoji_1f194); sSoftbanksMap.append(0xe22a, R.drawable.emoji_1f235); sSoftbanksMap.append(0xe22b, R.drawable.emoji_1f233); sSoftbanksMap.append(0xe22c, R.drawable.emoji_1f22f); sSoftbanksMap.append(0xe22d, R.drawable.emoji_1f23a); sSoftbanksMap.append(0xe22e, R.drawable.emoji_1f446); sSoftbanksMap.append(0xe22f, R.drawable.emoji_1f447); sSoftbanksMap.append(0xe230, R.drawable.emoji_1f448); sSoftbanksMap.append(0xe231, R.drawable.emoji_1f449); sSoftbanksMap.append(0xe232, R.drawable.emoji_2b06); sSoftbanksMap.append(0xe233, R.drawable.emoji_2b07); sSoftbanksMap.append(0xe234, R.drawable.emoji_27a1); sSoftbanksMap.append(0xe235, R.drawable.emoji_1f519); sSoftbanksMap.append(0xe236, R.drawable.emoji_2197); sSoftbanksMap.append(0xe237, R.drawable.emoji_2196); sSoftbanksMap.append(0xe238, R.drawable.emoji_2198); sSoftbanksMap.append(0xe239, R.drawable.emoji_2199); sSoftbanksMap.append(0xe23a, R.drawable.emoji_25b6); sSoftbanksMap.append(0xe23b, R.drawable.emoji_25c0); sSoftbanksMap.append(0xe23c, R.drawable.emoji_23e9); sSoftbanksMap.append(0xe23d, R.drawable.emoji_23ea); sSoftbanksMap.append(0xe23e, R.drawable.emoji_1f52e); sSoftbanksMap.append(0xe23f, R.drawable.emoji_2648); sSoftbanksMap.append(0xe240, R.drawable.emoji_2649); sSoftbanksMap.append(0xe241, R.drawable.emoji_264a); sSoftbanksMap.append(0xe242, R.drawable.emoji_264b); sSoftbanksMap.append(0xe243, R.drawable.emoji_264c); sSoftbanksMap.append(0xe244, R.drawable.emoji_264d); sSoftbanksMap.append(0xe245, R.drawable.emoji_264e); sSoftbanksMap.append(0xe246, R.drawable.emoji_264f); sSoftbanksMap.append(0xe247, R.drawable.emoji_2650); sSoftbanksMap.append(0xe248, R.drawable.emoji_2651); sSoftbanksMap.append(0xe249, R.drawable.emoji_2652); sSoftbanksMap.append(0xe24a, R.drawable.emoji_2653); sSoftbanksMap.append(0xe24b, R.drawable.emoji_26ce); sSoftbanksMap.append(0xe24c, R.drawable.emoji_1f51d); sSoftbanksMap.append(0xe24d, R.drawable.emoji_1f197); sSoftbanksMap.append(0xe24e, R.drawable.emoji_00a9); sSoftbanksMap.append(0xe24f, R.drawable.emoji_00ae); sSoftbanksMap.append(0xe250, R.drawable.emoji_1f4f3); sSoftbanksMap.append(0xe251, R.drawable.emoji_1f4f4); sSoftbanksMap.append(0xe252, R.drawable.emoji_26a0); sSoftbanksMap.append(0xe253, R.drawable.emoji_1f481); sSoftbanksMap.append(0xe301, R.drawable.emoji_1f4c3); sSoftbanksMap.append(0xe302, R.drawable.emoji_1f454); sSoftbanksMap.append(0xe303, R.drawable.emoji_1f33a); sSoftbanksMap.append(0xe304, R.drawable.emoji_1f337); sSoftbanksMap.append(0xe305, R.drawable.emoji_1f33b); sSoftbanksMap.append(0xe306, R.drawable.emoji_1f490); sSoftbanksMap.append(0xe307, R.drawable.emoji_1f334); sSoftbanksMap.append(0xe308, R.drawable.emoji_1f335); sSoftbanksMap.append(0xe309, R.drawable.emoji_1f6be); sSoftbanksMap.append(0xe30a, R.drawable.emoji_1f3a7); sSoftbanksMap.append(0xe30b, R.drawable.emoji_1f376); sSoftbanksMap.append(0xe30c, R.drawable.emoji_1f37b); sSoftbanksMap.append(0xe30d, R.drawable.emoji_3297); sSoftbanksMap.append(0xe30e, R.drawable.emoji_1f6ac); sSoftbanksMap.append(0xe30f, R.drawable.emoji_1f48a); sSoftbanksMap.append(0xe310, R.drawable.emoji_1f388); sSoftbanksMap.append(0xe311, R.drawable.emoji_1f4a3); sSoftbanksMap.append(0xe312, R.drawable.emoji_1f389); sSoftbanksMap.append(0xe313, R.drawable.emoji_2702); sSoftbanksMap.append(0xe314, R.drawable.emoji_1f380); sSoftbanksMap.append(0xe315, R.drawable.emoji_3299); sSoftbanksMap.append(0xe316, R.drawable.emoji_1f4bd); sSoftbanksMap.append(0xe317, R.drawable.emoji_1f4e3); sSoftbanksMap.append(0xe318, R.drawable.emoji_1f452); sSoftbanksMap.append(0xe319, R.drawable.emoji_1f457); sSoftbanksMap.append(0xe31a, R.drawable.emoji_1f461); sSoftbanksMap.append(0xe31b, R.drawable.emoji_1f462); sSoftbanksMap.append(0xe31c, R.drawable.emoji_1f484); sSoftbanksMap.append(0xe31d, R.drawable.emoji_1f485); sSoftbanksMap.append(0xe31e, R.drawable.emoji_1f486); sSoftbanksMap.append(0xe31f, R.drawable.emoji_1f487); sSoftbanksMap.append(0xe320, R.drawable.emoji_1f488); sSoftbanksMap.append(0xe321, R.drawable.emoji_1f458); sSoftbanksMap.append(0xe322, R.drawable.emoji_1f459); sSoftbanksMap.append(0xe323, R.drawable.emoji_1f45c); sSoftbanksMap.append(0xe324, R.drawable.emoji_1f3ac); sSoftbanksMap.append(0xe325, R.drawable.emoji_1f514); sSoftbanksMap.append(0xe326, R.drawable.emoji_1f3b6); sSoftbanksMap.append(0xe327, R.drawable.emoji_1f493); sSoftbanksMap.append(0xe328, R.drawable.emoji_1f48c); sSoftbanksMap.append(0xe329, R.drawable.emoji_1f498); sSoftbanksMap.append(0xe32a, R.drawable.emoji_1f499); sSoftbanksMap.append(0xe32b, R.drawable.emoji_1f49a); sSoftbanksMap.append(0xe32c, R.drawable.emoji_1f49b); sSoftbanksMap.append(0xe32d, R.drawable.emoji_1f49c); sSoftbanksMap.append(0xe32e, R.drawable.emoji_2728); sSoftbanksMap.append(0xe32f, R.drawable.emoji_2b50); sSoftbanksMap.append(0xe330, R.drawable.emoji_1f4a8); sSoftbanksMap.append(0xe331, R.drawable.emoji_1f4a6); sSoftbanksMap.append(0xe332, R.drawable.emoji_2b55); sSoftbanksMap.append(0xe333, R.drawable.emoji_2716); sSoftbanksMap.append(0xe334, R.drawable.emoji_1f4a2); sSoftbanksMap.append(0xe335, R.drawable.emoji_1f31f); sSoftbanksMap.append(0xe336, R.drawable.emoji_2754); sSoftbanksMap.append(0xe337, R.drawable.emoji_2755); sSoftbanksMap.append(0xe338, R.drawable.emoji_1f375); sSoftbanksMap.append(0xe339, R.drawable.emoji_1f35e); sSoftbanksMap.append(0xe33a, R.drawable.emoji_1f366); sSoftbanksMap.append(0xe33b, R.drawable.emoji_1f35f); sSoftbanksMap.append(0xe33c, R.drawable.emoji_1f361); sSoftbanksMap.append(0xe33d, R.drawable.emoji_1f358); sSoftbanksMap.append(0xe33e, R.drawable.emoji_1f35a); sSoftbanksMap.append(0xe33f, R.drawable.emoji_1f35d); sSoftbanksMap.append(0xe340, R.drawable.emoji_1f35c); sSoftbanksMap.append(0xe341, R.drawable.emoji_1f35b); sSoftbanksMap.append(0xe342, R.drawable.emoji_1f359); sSoftbanksMap.append(0xe343, R.drawable.emoji_1f362); sSoftbanksMap.append(0xe344, R.drawable.emoji_1f363); sSoftbanksMap.append(0xe345, R.drawable.emoji_1f34e); sSoftbanksMap.append(0xe346, R.drawable.emoji_1f34a); sSoftbanksMap.append(0xe347, R.drawable.emoji_1f353); sSoftbanksMap.append(0xe348, R.drawable.emoji_1f349); sSoftbanksMap.append(0xe349, R.drawable.emoji_1f345); sSoftbanksMap.append(0xe34a, R.drawable.emoji_1f346); sSoftbanksMap.append(0xe34b, R.drawable.emoji_1f382); sSoftbanksMap.append(0xe34c, R.drawable.emoji_1f371); sSoftbanksMap.append(0xe34d, R.drawable.emoji_1f372); sSoftbanksMap.append(0xe401, R.drawable.emoji_1f625); sSoftbanksMap.append(0xe402, R.drawable.emoji_1f60f); sSoftbanksMap.append(0xe403, R.drawable.emoji_1f614); sSoftbanksMap.append(0xe404, R.drawable.emoji_1f601); sSoftbanksMap.append(0xe405, R.drawable.emoji_1f609); sSoftbanksMap.append(0xe406, R.drawable.emoji_1f623); sSoftbanksMap.append(0xe407, R.drawable.emoji_1f616); sSoftbanksMap.append(0xe408, R.drawable.emoji_1f62a); sSoftbanksMap.append(0xe409, R.drawable.emoji_1f445); sSoftbanksMap.append(0xe40a, R.drawable.emoji_1f606); sSoftbanksMap.append(0xe40b, R.drawable.emoji_1f628); sSoftbanksMap.append(0xe40c, R.drawable.emoji_1f637); sSoftbanksMap.append(0xe40d, R.drawable.emoji_1f633); sSoftbanksMap.append(0xe40e, R.drawable.emoji_1f612); sSoftbanksMap.append(0xe40f, R.drawable.emoji_1f630); sSoftbanksMap.append(0xe410, R.drawable.emoji_1f632); sSoftbanksMap.append(0xe411, R.drawable.emoji_1f62d); sSoftbanksMap.append(0xe412, R.drawable.emoji_1f602); sSoftbanksMap.append(0xe413, R.drawable.emoji_1f622); sSoftbanksMap.append(0xe414, R.drawable.emoji_263a); sSoftbanksMap.append(0xe415, R.drawable.emoji_1f605); sSoftbanksMap.append(0xe416, R.drawable.emoji_1f621); sSoftbanksMap.append(0xe417, R.drawable.emoji_1f61a); sSoftbanksMap.append(0xe418, R.drawable.emoji_1f618); sSoftbanksMap.append(0xe419, R.drawable.emoji_1f440); sSoftbanksMap.append(0xe41a, R.drawable.emoji_1f443); sSoftbanksMap.append(0xe41b, R.drawable.emoji_1f442); sSoftbanksMap.append(0xe41c, R.drawable.emoji_1f444); sSoftbanksMap.append(0xe41d, R.drawable.emoji_1f64f); sSoftbanksMap.append(0xe41e, R.drawable.emoji_1f44b); sSoftbanksMap.append(0xe41f, R.drawable.emoji_1f44f); sSoftbanksMap.append(0xe420, R.drawable.emoji_1f44c); sSoftbanksMap.append(0xe421, R.drawable.emoji_1f44e); sSoftbanksMap.append(0xe422, R.drawable.emoji_1f450); sSoftbanksMap.append(0xe423, R.drawable.emoji_1f645); sSoftbanksMap.append(0xe424, R.drawable.emoji_1f646); sSoftbanksMap.append(0xe425, R.drawable.emoji_1f491); sSoftbanksMap.append(0xe426, R.drawable.emoji_1f647); sSoftbanksMap.append(0xe427, R.drawable.emoji_1f64c); sSoftbanksMap.append(0xe428, R.drawable.emoji_1f46b); sSoftbanksMap.append(0xe429, R.drawable.emoji_1f46f); sSoftbanksMap.append(0xe42a, R.drawable.emoji_1f3c0); sSoftbanksMap.append(0xe42b, R.drawable.emoji_1f3c8); sSoftbanksMap.append(0xe42c, R.drawable.emoji_1f3b1); sSoftbanksMap.append(0xe42d, R.drawable.emoji_1f3ca); sSoftbanksMap.append(0xe42e, R.drawable.emoji_1f699); sSoftbanksMap.append(0xe42f, R.drawable.emoji_1f69a); sSoftbanksMap.append(0xe430, R.drawable.emoji_1f692); sSoftbanksMap.append(0xe431, R.drawable.emoji_1f691); sSoftbanksMap.append(0xe432, R.drawable.emoji_1f693); sSoftbanksMap.append(0xe433, R.drawable.emoji_1f3a2); sSoftbanksMap.append(0xe434, R.drawable.emoji_1f687); sSoftbanksMap.append(0xe435, R.drawable.emoji_1f684); sSoftbanksMap.append(0xe436, R.drawable.emoji_1f38d); sSoftbanksMap.append(0xe437, R.drawable.emoji_1f49d); sSoftbanksMap.append(0xe438, R.drawable.emoji_1f38e); sSoftbanksMap.append(0xe439, R.drawable.emoji_1f393); sSoftbanksMap.append(0xe43a, R.drawable.emoji_1f392); sSoftbanksMap.append(0xe43b, R.drawable.emoji_1f38f); sSoftbanksMap.append(0xe43c, R.drawable.emoji_1f302); sSoftbanksMap.append(0xe43d, R.drawable.emoji_1f492); sSoftbanksMap.append(0xe43e, R.drawable.emoji_1f30a); sSoftbanksMap.append(0xe43f, R.drawable.emoji_1f367); sSoftbanksMap.append(0xe440, R.drawable.emoji_1f387); sSoftbanksMap.append(0xe441, R.drawable.emoji_1f41a); sSoftbanksMap.append(0xe442, R.drawable.emoji_1f390); sSoftbanksMap.append(0xe443, R.drawable.emoji_1f300); sSoftbanksMap.append(0xe444, R.drawable.emoji_1f33e); sSoftbanksMap.append(0xe445, R.drawable.emoji_1f383); sSoftbanksMap.append(0xe446, R.drawable.emoji_1f391); sSoftbanksMap.append(0xe447, R.drawable.emoji_1f343); sSoftbanksMap.append(0xe448, R.drawable.emoji_1f385); sSoftbanksMap.append(0xe449, R.drawable.emoji_1f305); sSoftbanksMap.append(0xe44a, R.drawable.emoji_1f307); sSoftbanksMap.append(0xe44b, R.drawable.emoji_1f303); sSoftbanksMap.append(0xe44b, R.drawable.emoji_1f30c); sSoftbanksMap.append(0xe44c, R.drawable.emoji_1f308); sSoftbanksMap.append(0xe501, R.drawable.emoji_1f3e9); sSoftbanksMap.append(0xe502, R.drawable.emoji_1f3a8); sSoftbanksMap.append(0xe503, R.drawable.emoji_1f3a9); sSoftbanksMap.append(0xe504, R.drawable.emoji_1f3ec); sSoftbanksMap.append(0xe505, R.drawable.emoji_1f3ef); sSoftbanksMap.append(0xe506, R.drawable.emoji_1f3f0); sSoftbanksMap.append(0xe507, R.drawable.emoji_1f3a6); sSoftbanksMap.append(0xe508, R.drawable.emoji_1f3ed); sSoftbanksMap.append(0xe509, R.drawable.emoji_1f5fc); sSoftbanksMap.append(0xe50b, R.drawable.emoji_1f1ef_1f1f5); sSoftbanksMap.append(0xe50c, R.drawable.emoji_1f1fa_1f1f8); sSoftbanksMap.append(0xe50d, R.drawable.emoji_1f1eb_1f1f7); sSoftbanksMap.append(0xe50e, R.drawable.emoji_1f1e9_1f1ea); sSoftbanksMap.append(0xe50f, R.drawable.emoji_1f1ee_1f1f9); sSoftbanksMap.append(0xe510, R.drawable.emoji_1f1ec_1f1e7); sSoftbanksMap.append(0xe511, R.drawable.emoji_1f1ea_1f1f8); sSoftbanksMap.append(0xe512, R.drawable.emoji_1f1f7_1f1fa); sSoftbanksMap.append(0xe513, R.drawable.emoji_1f1e8_1f1f3); sSoftbanksMap.append(0xe514, R.drawable.emoji_1f1f0_1f1f7); sSoftbanksMap.append(0xe515, R.drawable.emoji_1f471); sSoftbanksMap.append(0xe516, R.drawable.emoji_1f472); sSoftbanksMap.append(0xe517, R.drawable.emoji_1f473); sSoftbanksMap.append(0xe518, R.drawable.emoji_1f474); sSoftbanksMap.append(0xe519, R.drawable.emoji_1f475); sSoftbanksMap.append(0xe51a, R.drawable.emoji_1f476); sSoftbanksMap.append(0xe51b, R.drawable.emoji_1f477); sSoftbanksMap.append(0xe51c, R.drawable.emoji_1f478); sSoftbanksMap.append(0xe51d, R.drawable.emoji_1f5fd); sSoftbanksMap.append(0xe51e, R.drawable.emoji_1f482); sSoftbanksMap.append(0xe51f, R.drawable.emoji_1f483); sSoftbanksMap.append(0xe520, R.drawable.emoji_1f42c); sSoftbanksMap.append(0xe521, R.drawable.emoji_1f426); sSoftbanksMap.append(0xe522, R.drawable.emoji_1f420); sSoftbanksMap.append(0xe523, R.drawable.emoji_1f423); sSoftbanksMap.append(0xe524, R.drawable.emoji_1f439); sSoftbanksMap.append(0xe525, R.drawable.emoji_1f41b); sSoftbanksMap.append(0xe526, R.drawable.emoji_1f418); sSoftbanksMap.append(0xe527, R.drawable.emoji_1f428); sSoftbanksMap.append(0xe528, R.drawable.emoji_1f412); sSoftbanksMap.append(0xe529, R.drawable.emoji_1f411); sSoftbanksMap.append(0xe52a, R.drawable.emoji_1f43a); sSoftbanksMap.append(0xe52b, R.drawable.emoji_1f42e); sSoftbanksMap.append(0xe52c, R.drawable.emoji_1f430); sSoftbanksMap.append(0xe52d, R.drawable.emoji_1f40d); sSoftbanksMap.append(0xe52e, R.drawable.emoji_1f414); sSoftbanksMap.append(0xe52f, R.drawable.emoji_1f417); sSoftbanksMap.append(0xe530, R.drawable.emoji_1f42b); sSoftbanksMap.append(0xe531, R.drawable.emoji_1f438); sSoftbanksMap.append(0xe532, R.drawable.emoji_1f170); sSoftbanksMap.append(0xe533, R.drawable.emoji_1f171); sSoftbanksMap.append(0xe534, R.drawable.emoji_1f18e); sSoftbanksMap.append(0xe535, R.drawable.emoji_1f17e); sSoftbanksMap.append(0xe536, R.drawable.emoji_1f43e); sSoftbanksMap.append(0xe537, R.drawable.emoji_2122); Log.d("emoji", String.format("init emoji cost: %dms", (System.currentTimeMillis() - start))); } public static QDQQFaceManager getInstance() { return sQDQQFaceManager; } @Override public Drawable getSpecialBoundsDrawable(CharSequence text) { return null; } @Override public int getSpecialDrawableMaxHeight() { return 0; } @Override public boolean maybeSoftBankEmoji(char c) { return ((c >> 12) == 0xe); } @Override public boolean maybeEmoji(int codePoint) { return codePoint > 0xff; } @Override public int getEmojiResource(int codePoint) { return sEmojisMap.get(codePoint); } @Override public int getSoftbankEmojiResource(char c) { return sSoftbanksMap.get(c); } @Override public int getDoubleUnicodeEmoji(int currentCodePoint, int nextCodePoint) { int icon = 0; if (nextCodePoint == 0x20e3) { switch (currentCodePoint) { case 0x0031: icon = R.drawable.emoji_0031; break; case 0x0032: icon = R.drawable.emoji_0032; break; case 0x0033: icon = R.drawable.emoji_0033; break; case 0x0034: icon = R.drawable.emoji_0034; break; case 0x0035: icon = R.drawable.emoji_0035; break; case 0x0036: icon = R.drawable.emoji_0036; break; case 0x0037: icon = R.drawable.emoji_0037; break; case 0x0038: icon = R.drawable.emoji_0038; break; case 0x0039: icon = R.drawable.emoji_0039; break; case 0x0030: icon = R.drawable.emoji_0030; break; case 0x0023: icon = R.drawable.emoji_0023; break; default: break; } } else { switch (currentCodePoint) { case 0x1f1ef: icon = (nextCodePoint == 0x1f1f5) ? R.drawable.emoji_1f1ef_1f1f5 : 0; break; case 0x1f1fa: icon = (nextCodePoint == 0x1f1f8) ? R.drawable.emoji_1f1fa_1f1f8 : 0; break; case 0x1f1eb: icon = (nextCodePoint == 0x1f1f7) ? R.drawable.emoji_1f1eb_1f1f7 : 0; break; case 0x1f1e9: icon = (nextCodePoint == 0x1f1ea) ? R.drawable.emoji_1f1e9_1f1ea : 0; break; case 0x1f1ee: icon = (nextCodePoint == 0x1f1f9) ? R.drawable.emoji_1f1ee_1f1f9 : 0; break; case 0x1f1ec: icon = (nextCodePoint == 0x1f1e7) ? R.drawable.emoji_1f1ec_1f1e7 : 0; break; case 0x1f1ea: icon = (nextCodePoint == 0x1f1f8) ? R.drawable.emoji_1f1ea_1f1f8 : 0; break; case 0x1f1f7: icon = (nextCodePoint == 0x1f1fa) ? R.drawable.emoji_1f1f7_1f1fa : 0; break; case 0x1f1e8: icon = (nextCodePoint == 0x1f1f3) ? R.drawable.emoji_1f1e8_1f1f3 : 0; break; case 0x1f1f0: icon = (nextCodePoint == 0x1f1f7) ? R.drawable.emoji_1f1f0_1f1f7 : 0; break; default: break; } } return icon; } @Override public int getQQfaceResource(CharSequence text) { Integer integer = sQQFaceMap.get(text.toString()); if (integer == null) { return 0; } return integer; } @Override public Drawable queryForDrawable(CharSequence text) { Integer integer = sQQFaceMap.get(text.toString()); if (integer == null) { return null; } return ContextCompat.getDrawable(QDApplication.getContext(), integer); } @Override public Drawable queryForDrawable(char c) { int res = sSoftbanksMap.get(c); if(res == 0){ return null; } return ContextCompat.getDrawable(QDApplication.getContext(), res); } @Override public Drawable queryForDrawable(int codePoint) { int res = sEmojisMap.get(codePoint); if(res == 0){ return null; } return ContextCompat.getDrawable(QDApplication.getContext(), res); } @Override public Drawable queryForDrawable(int firstCodePoint, int secondCodePint) { int res = getDoubleUnicodeEmoji(firstCodePoint, secondCodePint); if(res == 0){ return null; } return ContextCompat.getDrawable(QDApplication.getContext(), res); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/ArchTestActivity.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.activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.arch.annotation.ActivityScheme; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseActivity; import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment; import butterknife.BindView; import butterknife.ButterKnife; @ActivityScheme(name = "arch", useRefreshIfCurrentMatched = true, required = {"aa", "bb=3"}, keysWithBoolValue = {"aa"}) public class ArchTestActivity extends BaseActivity { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); View root = LayoutInflater.from(this).inflate(R.layout.activity_arch_test, null); ButterKnife.bind(this, root); initTopBar(); setContentView(root); } private void initTopBar() { mTopBar.setBackgroundColor(ContextCompat.getColor(this, R.color.app_color_theme_4)); mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); overridePendingTransition(R.anim.slide_still, R.anim.slide_out_right); } }); mTopBar.setTitle("Arch Test"); QDArchTestFragment.injectEntrance(mTopBar); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/LauncherActivity.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.qmuidemo.activity import android.Manifest import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import com.qmuiteam.qmui.arch.QMUILatestVisit import com.qmuiteam.qmui.arch.annotation.ActivityScheme import com.qmuiteam.qmuidemo.QDMainActivity /** * @author cginechen * @date 2016-12-08 */ @ActivityScheme(name = "launcher") class LauncherActivity : AppCompatActivity() { private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) { var intent = QMUILatestVisit.intentOfLatestVisit(this) if (intent == null) { intent = Intent(this, QDMainActivity::class.java) } startActivity(intent) finish() } else { Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) { finish() return } permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } override fun finish() { super.finish() overridePendingTransition(0, 0) } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/QDPhotoPickerActivity.kt ================================================ package com.qmuiteam.qmuidemo.activity import com.qmuiteam.photo.activity.QMUIPhotoPickerActivity class QDPhotoPickerActivity: QMUIPhotoPickerActivity() { } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/TestArchInViewPagerActivity.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.activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.QMUIFragmentPagerAdapter; import com.qmuiteam.qmui.widget.QMUIViewPager; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseActivity; import com.qmuiteam.qmuidemo.fragment.components.QDCollapsingTopBarLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentScrollableModeFragment; import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDFitSystemWindowViewPagerFragment; import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDViewPagerFragment; import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; public class TestArchInViewPagerActivity extends BaseActivity { @BindView(R.id.pager) QMUIViewPager mViewPager; @BindView(R.id.tabs) QMUITabSegment mTabSegment; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); View root = LayoutInflater.from(this).inflate(R.layout.fragment_fsw_viewpager, null); ButterKnife.bind(this, root); setContentView(root); initPagers(); } private void initPagers() { QMUIFragmentPagerAdapter pagerAdapter = new QMUIFragmentPagerAdapter(getSupportFragmentManager()) { @Override public QMUIFragment createFragment(int position) { switch (position) { case 0: return new QDTabSegmentScrollableModeFragment(); case 1: return new QDCollapsingTopBarLayoutFragment(); case 2: return new QDFitSystemWindowViewPagerFragment(); case 3: default: return new QDViewPagerFragment(); } } @Override public int getCount() { return 4; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return "TabSegment"; case 1: return "CTopBar"; case 2: return "IViewPager"; case 3: default: return "ViewPager"; } } }; mViewPager.setAdapter(pagerAdapter); mTabSegment.setupWithViewPager(mViewPager); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/activity/TranslucentActivity.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.activity; import android.os.Bundle; import androidx.core.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.arch.record.RecordArgumentEditor; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseActivity; import butterknife.BindView; import butterknife.ButterKnife; /** * 沉浸式状态栏的调用示例。 * Created by Kayo on 2016/12/12. */ @LatestVisitRecord public class TranslucentActivity extends BaseActivity { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); View root = LayoutInflater.from(this).inflate(R.layout.activity_translucent, null); ButterKnife.bind(this, root); initTopBar(); setContentView(root); if (getIntent().getBooleanExtra("test_activity", false)) { Toast.makeText(this, "恢复到最近阅读(Boolean)", Toast.LENGTH_SHORT).show(); } } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); overridePendingTransition(R.anim.slide_still, R.anim.slide_out_right); } }); mTopBar.setTitle("沉浸式状态栏示例"); } @Override public void onCollectLatestVisitArgument(RecordArgumentEditor editor) { editor.putBoolean("test_activity", true); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/adaptor/QDRecyclerViewAdapter.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.adaptor; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.TextView; import com.qmuiteam.qmuidemo.R; import java.util.ArrayList; import java.util.List; /** * Demo 中通用的 RecyclerView Adapter。 * Created by sm on 2015/5/3. */ public class QDRecyclerViewAdapter extends RecyclerView.Adapter { private List mItems; private AdapterView.OnItemClickListener mOnItemClickListener; public QDRecyclerViewAdapter() { mItems = new ArrayList<>(); } public static List generateDatas(int count) { ArrayList mDatas = new ArrayList<>(); for (int i = 0; i < count; i++) { mDatas.add(new Data(String.valueOf(i))); } return mDatas; } public void addItem(int position) { if (position > mItems.size()) return; mItems.add(position, new Data(String.valueOf(position))); notifyItemInserted(position); } public void removeItem(int position) { if (position >= mItems.size()) return; mItems.remove(position); notifyItemRemoved(position); } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View root = inflater.inflate(R.layout.recycler_view_item, viewGroup, false); return new ViewHolder(root, this); } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { Data data = mItems.get(i); viewHolder.setText(data.text); } @Override public int getItemCount() { return mItems.size(); } public void setItemCount(int count) { mItems.clear(); mItems.addAll(generateDatas(count)); notifyDataSetChanged(); } public void setOnItemClickListener(AdapterView.OnItemClickListener onItemClickListener) { mOnItemClickListener = onItemClickListener; } private void onItemHolderClick(RecyclerView.ViewHolder itemHolder) { if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick(null, itemHolder.itemView, itemHolder.getAdapterPosition(), itemHolder.getItemId()); } } public static class Data { public String text; public Data(String text) { this.text = text; } } public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private TextView mTextView; private QDRecyclerViewAdapter mAdapter; public ViewHolder(View itemView, QDRecyclerViewAdapter adapter) { super(itemView); itemView.setOnClickListener(this); mAdapter = adapter; mTextView = (TextView) itemView.findViewById(R.id.textView); } public void setText(String text) { mTextView.setText(text); } @Override public void onClick(View v) { mAdapter.onItemHolderClick(this); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/adaptor/QDSimpleAdapter.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.adaptor; import android.content.Context; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmuidemo.R; import java.util.List; /** * @author cginechen * @date 2017-03-30 */ public class QDSimpleAdapter extends BaseAdapter { private Context mContext; private List mData; public QDSimpleAdapter(Context context, List data) { mContext = context; mData = data; } public void setData(List data) { mData = data; } @Override public int getCount() { return mData.size(); } @Override public String getItem(int position) { return mData.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ItemView itemView; if (convertView == null) { itemView = new ItemView(mContext); itemView.setLayoutParams(new AbsListView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } else { itemView = (ItemView) convertView; } itemView.setText(getItem(position)); return itemView; } public void remove(int position) { mData.remove(position); notifyDataSetChanged(); } static class ItemView extends FrameLayout { private TextView textView; public ItemView(Context context) { super(context); textView = new TextView(context); int paddingHor = QMUIDisplayHelper.dp2px(context, 12); int paddingVer = QMUIDisplayHelper.dp2px(context, 6); setPadding(paddingHor, paddingVer, paddingHor, paddingVer); addView(textView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, QMUIDisplayHelper.dp2px(context, 64))); int paddingTvHor = QMUIDisplayHelper.dp2px(context, 16); QMUIViewHelper.setBackgroundKeepingPadding(textView, QMUIResHelper.getAttrDrawable(context, R.attr.qmui_skin_support_s_list_item_bg_1)); textView.setPadding(paddingTvHor, 0, paddingTvHor, 0); textView.setGravity(Gravity.CENTER_VERTICAL); } public void setText(String text) { textView.setText(text); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseActivity.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.base; import android.annotation.SuppressLint; import android.content.Intent; import com.qmuiteam.qmui.arch.QMUIActivity; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.manager.QDUpgradeManager; import static com.qmuiteam.qmuidemo.QDApplication.getContext; @SuppressLint("Registered") public class BaseActivity extends QMUIActivity { @Override protected int backViewInitOffset() { return QMUIDisplayHelper.dp2px(getContext(), 100); } @Override protected void onResume() { super.onResume(); QDUpgradeManager.getInstance(getContext()).runUpgradeTipTaskIfExist(this); } @Override public Intent onLastActivityFinish() { return new Intent(this, QDMainActivity.class); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragment.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.base; import android.content.Context; import android.content.Intent; import android.util.Log; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.SwipeBackLayout; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.fragment.home.HomeFragment; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.manager.QDUpgradeManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; /** * Created by cgspine on 2018/1/7. */ public abstract class BaseFragment extends QMUIFragment { private static final String TAG = "BaseFragment"; public BaseFragment() { } @Override protected int backViewInitOffset(Context context, int dragDirection, int moveEdge) { if (moveEdge == SwipeBackLayout.EDGE_TOP || moveEdge == SwipeBackLayout.EDGE_BOTTOM) { return 0; } return QMUIDisplayHelper.dp2px(context, 100); } @Override public void onResume() { super.onResume(); QDUpgradeManager.getInstance(getContext()).runUpgradeTipTaskIfExist(getActivity()); Log.i(TAG, getClass().getSimpleName() + " onResume"); } @Override public void onStart() { super.onStart(); Log.i(TAG, getClass().getSimpleName() + " onStart"); } @Override public void onPause() { super.onPause(); Log.i(TAG, getClass().getSimpleName() + " onPause"); } @Override public void onStop() { super.onStop(); Log.i(TAG, getClass().getSimpleName() + " onStop"); } @Override public Object onLastFragmentFinish() { return new HomeFragment(); } protected void goToWebExplorer(@NonNull String url, @Nullable String title) { Intent intent = QDMainActivity.createWebExplorerIntent(getContext(), url, title); startActivity(intent); } protected void injectDocToTopBar(QMUITopBar topBar) { final QDItemDescription description = QDDataManager.getInstance().getDescription(this.getClass()); if (description != null) { topBar.addRightTextButton("DOC", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { goToWebExplorer(description.getDocUrl(), description.getName()); } }); } } protected void injectDocToTopBar(QMUITopBarLayout topBar) { final QDItemDescription description = QDDataManager.getInstance().getDescription(this.getClass()); if (description != null) { topBar.addRightTextButton("DOC", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { goToWebExplorer(description.getDocUrl(), description.getName()); } }); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseFragmentActivity.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.base; import com.qmuiteam.qmui.arch.QMUIFragmentActivity; /** * Created by cgspine on 2018/1/7. */ public abstract class BaseFragmentActivity extends QMUIFragmentActivity { } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/BaseRecyclerAdapter.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.base; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * @author cginechen * @date 2016-10-19 */ public abstract class BaseRecyclerAdapter extends RecyclerView.Adapter { private List mData = new ArrayList<>(); private final Context mContext; private LayoutInflater mInflater; private OnItemClickListener mClickListener; private OnItemLongClickListener mLongClickListener; public BaseRecyclerAdapter(Context ctx, @Nullable List list) { if(list != null){ mData.addAll(list); } mContext = ctx; mInflater = LayoutInflater.from(ctx); } public void setData(@Nullable List list) { mData.clear(); if(list != null){ mData.addAll(list); } notifyDataSetChanged(); } public void remove(int pos){ mData.remove(pos); notifyItemRemoved(pos); } @Override public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final RecyclerViewHolder holder = new RecyclerViewHolder(mContext, mInflater.inflate(getItemLayoutId(viewType), parent, false)); if (mClickListener != null) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mClickListener.onItemClick(holder.itemView, holder.getLayoutPosition()); } }); } if (mLongClickListener != null) { holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { mLongClickListener.onItemLongClick(holder.itemView, holder.getLayoutPosition()); return true; } }); } return holder; } @Override public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { bindData(holder, position, mData.get(position)); } public T getItem(int pos){ return mData.get(pos); } @Override public int getItemCount() { return mData.size(); } public void add(int pos, T item) { mData.add(pos, item); notifyItemInserted(pos); } public void prepend(@NonNull List items){ mData.addAll(0, items); notifyDataSetChanged(); } public void append(@NonNull List items){ mData.addAll(items); notifyDataSetChanged(); } public void delete(int pos) { mData.remove(pos); notifyItemRemoved(pos); } public void setOnItemClickListener(OnItemClickListener listener) { mClickListener = listener; } public void setOnItemLongClickListener(OnItemLongClickListener listener) { mLongClickListener = listener; } @SuppressWarnings("SameReturnValue") abstract public int getItemLayoutId(int viewType); abstract public void bindData(@NonNull RecyclerViewHolder holder, int position, @NonNull T item); public interface OnItemClickListener { void onItemClick(View itemView, int pos); } public interface OnItemLongClickListener { void onItemLongClick(View itemView, int pos); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/ComposeBaseFragment.kt ================================================ package com.qmuiteam.qmuidemo.base import android.view.View import android.widget.FrameLayout import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.lifecycle.ViewTreeViewModelStoreOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.qmuiteam.compose.core.provider.QMUIWindowInsetsProvider import com.qmuiteam.qmui.kotlin.matchParent abstract class ComposeBaseFragment(): BaseFragment() { override fun onCreateView(): View { return object: FrameLayout(requireContext()){ private val composeView = ComposeView(requireContext()).apply { setContent { QMUIWindowInsetsProvider { PageContent() } } }.apply { ViewTreeLifecycleOwner.set(this, this@ComposeBaseFragment) ViewTreeViewModelStoreOwner.set(this, this@ComposeBaseFragment) setViewTreeSavedStateRegistryOwner(this@ComposeBaseFragment) } init { addView(composeView, LayoutParams(matchParent, matchParent)) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val wm = MeasureSpec.getMode(widthMeasureSpec) val ws = MeasureSpec.getSize(widthMeasureSpec).coerceAtMost(0x2FFFF) val hm = MeasureSpec.getMode(heightMeasureSpec) val hs = MeasureSpec.getSize(heightMeasureSpec).coerceAtMost(0x2FFFF) super.onMeasure( MeasureSpec.makeMeasureSpec(ws, wm), MeasureSpec.makeMeasureSpec(hs, hm) ) } } } @Composable protected abstract fun PageContent() } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/base/RecyclerViewHolder.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.base; import android.content.Context; import androidx.recyclerview.widget.RecyclerView; import android.util.SparseArray; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; /** * @author cginechen * @date 2016-10-19 */ public class RecyclerViewHolder extends RecyclerView.ViewHolder { private SparseArray mViews; public RecyclerViewHolder(Context ctx, View itemView) { super(itemView); mViews = new SparseArray<>(); } @SuppressWarnings("unchecked") private T findViewById(int viewId) { View view = mViews.get(viewId); if (view == null) { view = itemView.findViewById(viewId); mViews.put(viewId, view); } return (T) view; } public View getView(int viewId) { return findViewById(viewId); } public TextView getTextView(int viewId) { return (TextView) getView(viewId); } public Button getButton(int viewId) { return (Button) getView(viewId); } public ImageView getImageView(int viewId) { return (ImageView) getView(viewId); } public ImageButton getImageButton(int viewId) { return (ImageButton) getView(viewId); } public EditText getEditText(int viewId) { return (EditText) getView(viewId); } public RecyclerViewHolder setText(int viewId, String value) { TextView view = findViewById(viewId); view.setText(value); return this; } public RecyclerViewHolder setBackground(int viewId, int resId) { View view = findViewById(viewId); view.setBackgroundResource(resId); return this; } public RecyclerViewHolder setClickListener(int viewId, View.OnClickListener listener) { View view = findViewById(viewId); view.setOnClickListener(listener); return this; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/decorator/DividerItemDecoration.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.decorator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.View; /** * @author cginechen * @date 2016-10-21 */ public class DividerItemDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; private Drawable mDivider; private int mOrientation; public DividerItemDecoration(Context context, int orientation) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); setOrientation(orientation); } public void setOrientation(int orientation) { if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { throw new IllegalArgumentException("invalid orientation"); } mOrientation = orientation; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (mOrientation == VERTICAL_LIST) { drawVertical(c, parent); } else { drawHorizontal(c, parent); } } public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } public void drawHorizontal(Canvas c, RecyclerView parent) { final int top = parent.getPaddingTop(); final int bottom = parent.getHeight() - parent.getPaddingBottom(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child)); final int right = left + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/decorator/GridDividerItemDecoration.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.decorator; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.view.View; import com.qmuiteam.qmui.skin.IQMUISkinHandlerDecoration; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmuidemo.R; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; import org.jetbrains.annotations.NotNull; /** * @author cginechen * @date 2016-10-21 */ public class GridDividerItemDecoration extends RecyclerView.ItemDecoration implements IQMUISkinHandlerDecoration { private Paint mDividerPaint = new Paint(); private int mSpanCount; private final int mDividerAttr; public GridDividerItemDecoration(Context context, int spanCount) { this(context, spanCount, R.attr.qmui_skin_support_color_separator, 1f); } public GridDividerItemDecoration(Context context, int spanCount, int dividerColorAttr, float dividerWidth) { mSpanCount = spanCount; mDividerAttr = dividerColorAttr; mDividerPaint.setStrokeWidth(dividerWidth); mDividerPaint.setStyle(Paint.Style.STROKE); mDividerPaint.setColor(QMUIResHelper.getAttrColor(context, dividerColorAttr)); } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); int position = parent.getChildLayoutPosition(child); int column = (position + 1) % mSpanCount; final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int childBottom = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); final int childRight = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child)); if (childBottom < parent.getHeight()) { c.drawLine(child.getLeft(), childBottom, childRight, childBottom, mDividerPaint); } if (column < mSpanCount) { c.drawLine(childRight, child.getTop(), childRight, childBottom, mDividerPaint); } } } @Override public void handle(@NotNull RecyclerView recyclerView, @NotNull QMUISkinManager manager, int skinIndex, @NotNull Resources.Theme theme) { mDividerPaint.setColor(QMUIResHelper.getAttrColor(theme, mDividerAttr)); recyclerView.invalidate(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDAboutFragment.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.qmuidemo.fragment import android.os.Bundle import com.qmuiteam.qmuidemo.base.BaseFragment import butterknife.BindView import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView import android.view.LayoutInflater import android.view.View import android.widget.TextView import butterknife.ButterKnife import com.qmuiteam.qmui.util.QMUIPackageHelper import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment import com.qmuiteam.qmui.arch.QMUIFragment import com.qmuiteam.qmui.arch.QMUIFragment.TransitionConfig import com.qmuiteam.qmui.arch.SwipeBackLayout.ViewMoveAction import com.qmuiteam.qmui.arch.SwipeBackLayout import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.manager.QDSchemeManager import java.text.SimpleDateFormat import java.util.* /** * 关于界面 * * * Created by Kayo on 2016/11/18. */ class QDAboutFragment : BaseFragment() { @JvmField @BindView(R.id.topbar) var mTopBar: QMUITopBarLayout? = null @JvmField @BindView(R.id.version) var mVersionTextView: TextView? = null @JvmField @BindView(R.id.about_list) var mAboutGroupListView: QMUIGroupListView? = null @JvmField @BindView(R.id.copyright) var mCopyrightTextView: TextView? = null override fun onCreateView(): View { val root = LayoutInflater.from(activity).inflate(R.layout.fragment_about, null) ButterKnife.bind(this, root) initTopBar() mVersionTextView!!.text = QMUIPackageHelper.getAppVersion(context) QMUIGroupListView.newSection(context) .addItemView(mAboutGroupListView!!.createItemView(resources.getString(R.string.about_item_homepage))) { val url = "https://qmuiteam.com/android" val bundle = Bundle() bundle.putString(QDWebExplorerFragment.EXTRA_URL, url) bundle.putString(QDWebExplorerFragment.EXTRA_TITLE, resources.getString(R.string.about_item_homepage)) val fragment: QMUIFragment = QDWebExplorerFragment() fragment.arguments = bundle startFragment(fragment) } .addItemView(mAboutGroupListView!!.createItemView(resources.getString(R.string.about_item_github))) { val url = "https://github.com/Tencent/QMUI_Android" val bundle = Bundle() bundle.putString(QDWebExplorerFragment.EXTRA_URL, url) bundle.putString(QDWebExplorerFragment.EXTRA_TITLE, resources.getString(R.string.about_item_github)) val fragment: QMUIFragment = QDWebExplorerFragment() fragment.arguments = bundle startFragment(fragment) } .addTo(mAboutGroupListView) val dateFormat = SimpleDateFormat("yyyy", Locale.CHINA) val currentYear = dateFormat.format(Date()) mCopyrightTextView!!.text = String.format(resources.getString(R.string.about_copyright), currentYear) return root } private fun initTopBar() { mTopBar!!.addLeftBackImageButton().setOnClickListener { popBackStack() } mTopBar!!.setTitle(resources.getString(R.string.about_title)) } override fun onFetchTransitionConfig(): TransitionConfig { return SCALE_TRANSITION_CONFIG } override fun dragViewMoveAction(): ViewMoveAction { return SwipeBackLayout.MOVE_VIEW_TOP_TO_BOTTOM } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.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.qmuidemo.fragment import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import com.qmuiteam.compose.core.ex.drawBottomSeparator import com.qmuiteam.compose.modal.* import com.qmuiteam.compose.core.ui.* import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmui.widget.dialog.QMUIDialog import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.ComposeBaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget @Widget(widgetClass = QMUIDialog::class, iconRes = R.mipmap.icon_grid_dialog) @LatestVisitRecord class QDDialogFragment() : ComposeBaseFragment() { @Composable override fun PageContent() { Column(modifier = Modifier.fillMaxSize()) { val scrollState = rememberLazyListState() QMUITopBarWithLazyScrollState( scrollState = scrollState, title = "QMUIDialog", leftItems = arrayListOf( QMUITopBarBackIconItem { popBackStack() } ), rightItems = arrayListOf( QMUITopBarTextItem("Test") { startFragment(QDAboutFragment()) } ) ) val view = LocalView.current LazyColumn( state = scrollState, modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) ) { item { QMUIItem( title = "消息类型对话框", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiDialog { modal -> QMUIDialogMsg(modal, "这是标题", "这是一丢丢有趣但是没啥用的内容", listOf( QMUIModalAction("取 消") { it.dismiss() }, QMUIModalAction("确 定") { Toast .makeText(view.context, "确定啦!!!", Toast.LENGTH_SHORT) .show() it.dismiss() } ) ) }.show() } } item { QMUIItem( title = "列表类型对话框", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiDialog { modal -> QMUIDialogList(modal, maxHeight = 500.dp) { items(200){ index -> QMUIItem(title = "第${index + 1}项") { Toast.makeText(view.context, "你点了第${index + 1}项", Toast.LENGTH_SHORT).show() } } } }.show() } } item { QMUIItem( title = "单选类型浮层", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiDialog { modal -> val list = remember { val items = arrayListOf() for(i in 0 until 500){ items.add("Item $i") } items } val markIndex by remember { mutableStateOf(20) } QMUIDialogMarkList( modal, maxHeight = 500.dp, list = list, markIndex = markIndex ) { _, index -> Toast.makeText(view.context, "你点了第${index + 1}项", Toast.LENGTH_SHORT).show() // modal.dismiss() } }.show() } } item { QMUIItem( title = "多选类型浮层", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiDialog { modal -> val list = remember { val items = arrayListOf() for(i in 0 until 500){ items.add("Item $i") } items } val checked = remember { mutableStateListOf(0, 5, 10, 20) } val disable = remember { mutableStateListOf(5, 10) } Column() { QMUIDialogMutiCheckList( modal, maxHeight = 500.dp, list = list, checked = checked.toSet(), disabled = disable.toSet() ) { _, index -> if(checked.contains(index)){ checked.remove(index) }else{ checked.add(index) } } QMUIDialogActions(modal = modal, actions = listOf( QMUIModalAction("取 消") { it.dismiss() }, QMUIModalAction("确 定") { Toast .makeText(view.context, "你选择了: ${checked.joinToString(",")}", Toast.LENGTH_SHORT) .show() it.dismiss() } )) } }.show() } } item { QMUIItem( title = "Toast", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiToast("这只是个 Toast!") } } item { QMUIItem( title = "BottomSheet(list)", drawBehind = { drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) } ) { view.qmuiBottomSheet { QMUIBottomSheetList(it) { items(200){ index -> QMUIItem(title = "第${index + 1}项") { Toast.makeText(view.context, "你点了第${index + 1}项", Toast.LENGTH_SHORT).show() } } } }.show() } } } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDWebExplorerFragment.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.fragment; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.LayoutInflater; import android.view.View; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.ZoomButtonsController; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmui.widget.webview.QMUIWebViewClient; import com.qmuiteam.qmui.widget.webview.QMUIWebViewContainer; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.view.QDWebView; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.net.URLDecoder; import butterknife.BindView; import butterknife.ButterKnife; /** * Created by cgspine on 2017/12/4. */ public class QDWebExplorerFragment extends BaseFragment { public static final String EXTRA_URL = "EXTRA_URL"; public static final String EXTRA_TITLE = "EXTRA_TITLE"; public static final String EXTRA_NEED_DECODE = "EXTRA_NEED_DECODE"; private final static int PROGRESS_PROCESS = 0; private final static int PROGRESS_GONE = 1; @BindView(R.id.topbar) protected QMUITopBarLayout mTopBarLayout; @BindView(R.id.webview_container) QMUIWebViewContainer mWebViewContainer; @BindView(R.id.progress_bar) ProgressBar mProgressBar; protected QDWebView mWebView; private String mUrl; private String mTitle; private ProgressHandler mProgressHandler; private boolean mIsPageFinished = false; private boolean mNeedDecodeUrl = false; @Override protected View onCreateView() { Bundle bundle = getArguments(); if (bundle != null) { String url = bundle.getString(EXTRA_URL); mTitle = bundle.getString(EXTRA_TITLE); mNeedDecodeUrl = bundle.getBoolean(EXTRA_NEED_DECODE, false); if (url != null && url.length() > 0) { handleUrl(url); } } mProgressHandler = new ProgressHandler(); View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_webview_explorer, null); ButterKnife.bind(this, view); initTopbar(); initWebView(); return view; } protected void initTopbar() { mTopBarLayout.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); updateTitle(mTitle); } private void updateTitle(String title) { if (title != null && !title.equals("")) { mTitle = title; mTopBarLayout.setTitle(mTitle); } } protected boolean needDispatchSafeAreaInset() { return false; } protected void initWebView() { mWebView = new QDWebView(getContext()); boolean needDispatchSafeAreaInset = needDispatchSafeAreaInset(); mWebViewContainer.addWebView(mWebView, needDispatchSafeAreaInset); mWebViewContainer.setCustomOnScrollChangeListener(new QMUIWebView.OnScrollChangeListener() { @Override public void onScrollChange(WebView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { onScrollWebContent(scrollX, scrollY, oldScrollX, oldScrollY); } }); FrameLayout.LayoutParams containerLp = (FrameLayout.LayoutParams) mWebViewContainer.getLayoutParams(); mWebViewContainer.setFitsSystemWindows(!needDispatchSafeAreaInset); containerLp.topMargin = needDispatchSafeAreaInset ? 0 : QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_topbar_height); mWebViewContainer.setLayoutParams(containerLp); mWebView.setDownloadListener(new DownloadListener() { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { boolean needConfirm = !url.startsWith("http://qmuiteam.com") && !url.startsWith("https://qmuiteam.com"); if (needConfirm) { final String finalURL = url; new QMUIDialog.MessageDialogBuilder(getContext()) .setMessage("确认下载此文件?") .addAction(R.string.cancel, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { dialog.dismiss(); popBackStack(); } }) .addAction(R.string.ok, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { dialog.dismiss(); doDownload(finalURL); popBackStack(); } }) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .show(); } else { doDownload(url); } } private void doDownload(String url) { } }); mWebView.setWebChromeClient(getWebViewChromeClient()); mWebView.setWebViewClient(getWebViewClient()); mWebView.requestFocus(View.FOCUS_DOWN); setZoomControlGone(mWebView); configWebView(mWebViewContainer, mWebView); mWebView.loadUrl(mUrl); } protected void configWebView(QMUIWebViewContainer webViewContainer, QMUIWebView webView) { } protected void onScrollWebContent(int scrollX, int scrollY, int oldScrollX, int oldScrollY) { } private void handleUrl(String url) { if (mNeedDecodeUrl) { String decodeURL; try { decodeURL = URLDecoder.decode(url, "utf-8"); } catch (UnsupportedEncodingException ignored) { decodeURL = url; } mUrl = decodeURL; } else { mUrl = url; } } protected WebChromeClient getWebViewChromeClient() { return new ExplorerWebViewChromeClient(this); } protected QMUIWebViewClient getWebViewClient() { return new ExplorerWebViewClient(needDispatchSafeAreaInset()); } private void sendProgressMessage(int progressType, int newProgress, int duration) { Message msg = new Message(); msg.what = progressType; msg.arg1 = newProgress; msg.arg2 = duration; mProgressHandler.sendMessage(msg); } @Override public void onDestroy() { super.onDestroy(); mWebViewContainer.destroy(); mWebView = null; } public static void setZoomControlGone(WebView webView) { webView.getSettings().setDisplayZoomControls(false); @SuppressWarnings("rawtypes") Class classType; Field field; try { classType = WebView.class; field = classType.getDeclaredField("mZoomButtonsController"); field.setAccessible(true); ZoomButtonsController zoomButtonsController = new ZoomButtonsController( webView); zoomButtonsController.getZoomControls().setVisibility(View.GONE); try { field.set(webView, zoomButtonsController); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } } catch (SecurityException | NoSuchFieldException e) { e.printStackTrace(); } } public static class ExplorerWebViewChromeClient extends WebChromeClient { private QDWebExplorerFragment mFragment; public ExplorerWebViewChromeClient(QDWebExplorerFragment fragment) { mFragment = fragment; } @Override public void onProgressChanged(WebView view, int newProgress) { super.onProgressChanged(view, newProgress); // 修改进度条 if (newProgress > mFragment.mProgressHandler.mDstProgressIndex) { mFragment.sendProgressMessage(PROGRESS_PROCESS, newProgress, 100); } } @Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); mFragment.updateTitle(view.getTitle()); } @Override public void onShowCustomView(View view, CustomViewCallback callback) { callback.onCustomViewHidden(); } @Override public void onHideCustomView() { } } protected class ExplorerWebViewClient extends QMUIWebViewClient { public ExplorerWebViewClient(boolean needDispatchSafeAreaInset) { super(needDispatchSafeAreaInset, true); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); if (QMUILangHelper.isNullOrEmpty(mTitle)) { updateTitle(view.getTitle()); } if (mProgressHandler.mDstProgressIndex == 0) { sendProgressMessage(PROGRESS_PROCESS, 30, 500); } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); sendProgressMessage(PROGRESS_GONE, 100, 0); if (QMUILangHelper.isNullOrEmpty(mTitle)) { updateTitle(view.getTitle()); } } } private class ProgressHandler extends Handler { private int mDstProgressIndex; private int mDuration; private ObjectAnimator mAnimator; @Override public void handleMessage(Message msg) { switch (msg.what) { case PROGRESS_PROCESS: mIsPageFinished = false; mDstProgressIndex = msg.arg1; mDuration = msg.arg2; mProgressBar.setVisibility(View.VISIBLE); if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); } mAnimator = ObjectAnimator.ofInt(mProgressBar, "progress", mDstProgressIndex); mAnimator.setDuration(mDuration); mAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mProgressBar.getProgress() == 100) { sendEmptyMessageDelayed(PROGRESS_GONE, 500); } } }); mAnimator.start(); break; case PROGRESS_GONE: mDstProgressIndex = 0; mDuration = 0; mProgressBar.setProgress(0); mProgressBar.setVisibility(View.GONE); if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); } mAnimator = ObjectAnimator.ofInt(mProgressBar, "progress", 0); mAnimator.setDuration(0); mAnimator.removeAllListeners(); mIsPageFinished = true; break; default: break; } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDBottomSheetFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Toast; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Collections; import java.util.List; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIBottomSheet} 的使用示例。 * Created by cgspine on 15/9/15. */ @Widget(widgetClass = QMUIBottomSheet.class, iconRes = R.mipmap.icon_grid_bottom_sheet) public class QDBottomSheetFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.listview) ListView mListView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_listview, null); ButterKnife.bind(this, view); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initListView(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initListView() { String[] listItems = new String[]{ "BottomSheet List:Simple", "BottomSheet List:With Icon", "BottomSheet List:GravityCenter", "BottomSheet List:With Title", "BottomSheet List:With Cancel Btn", "BottomSheet List:Drag Dismiss", "BottomSheet List:Many Items", "BottomSheet List:With Mark", "BottomSheet Grid" }; List data = new ArrayList<>(); Collections.addAll(data, listItems); mListView.setAdapter(new ArrayAdapter<>(getActivity(), R.layout.simple_list_item, data)); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { switch (position) { case 0: showSimpleBottomSheetList( false, false, false, null, 3, false, false); break; case 1: showSimpleBottomSheetList( false, false, true, null, 3, false, false); break; case 2: showSimpleBottomSheetList( true, false, false, null, 3, false, false); break; case 3: showSimpleBottomSheetList( true, false, false, "This is Title!!!", 3, false, false); break; case 4: showSimpleBottomSheetList( true, true, false, "This is Title!!!", 3, false, false); break; case 5: showSimpleBottomSheetList( true, true, false, "This is Title!!!", 3, true, false); break; case 6: showSimpleBottomSheetList( true, true, false, "This is Title!!!", 100, true, false); break; case 7: showSimpleBottomSheetList( false, true, false, "This is Title!!!", 100, true, true); break; case 8: showSimpleBottomSheetGrid(); break; } } }); } // ================================ 生成不同类型的BottomSheet private void showSimpleBottomSheetList(boolean gravityCenter, boolean addCancelBtn, boolean withIcon, CharSequence title, int itemCount, boolean allowDragDismiss, boolean withMark) { QMUIBottomSheet.BottomListSheetBuilder builder = new QMUIBottomSheet.BottomListSheetBuilder(getActivity()); builder.setGravityCenter(gravityCenter) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .setTitle(title) .setAddCancelBtn(addCancelBtn) .setAllowDrag(allowDragDismiss) .setNeedRightMark(withMark) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); Toast.makeText(getActivity(), "Item " + (position + 1), Toast.LENGTH_SHORT).show(); } }); if(withMark){ builder.setCheckedIndex(40); } for (int i = 1; i <= itemCount; i++) { if(withIcon){ builder.addItem(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_lab), "Item " + i); }else{ builder.addItem("Item " + i); } } builder.build().show(); } private void showSimpleBottomSheetGrid() { final int TAG_SHARE_WECHAT_FRIEND = 0; final int TAG_SHARE_WECHAT_MOMENT = 1; final int TAG_SHARE_WEIBO = 2; final int TAG_SHARE_CHAT = 3; final int TAG_SHARE_LOCAL = 4; QMUIBottomSheet.BottomGridSheetBuilder builder = new QMUIBottomSheet.BottomGridSheetBuilder(getActivity()); builder.addItem(R.mipmap.icon_more_operation_share_friend, "分享到微信", TAG_SHARE_WECHAT_FRIEND, QMUIBottomSheet.BottomGridSheetBuilder.FIRST_LINE) .addItem(R.mipmap.icon_more_operation_share_moment, "分享到朋友圈", TAG_SHARE_WECHAT_MOMENT, QMUIBottomSheet.BottomGridSheetBuilder.FIRST_LINE) .addItem(R.mipmap.icon_more_operation_share_weibo, "分享到微博", TAG_SHARE_WEIBO, QMUIBottomSheet.BottomGridSheetBuilder.FIRST_LINE) .addItem(R.mipmap.icon_more_operation_share_chat, "分享到私信", TAG_SHARE_CHAT, QMUIBottomSheet.BottomGridSheetBuilder.FIRST_LINE) .addItem(R.mipmap.icon_more_operation_save, "保存到本地", TAG_SHARE_LOCAL, QMUIBottomSheet.BottomGridSheetBuilder.SECOND_LINE) .setAddCancelBtn(true) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomGridSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView) { dialog.dismiss(); int tag = (int) itemView.getTag(); switch (tag) { case TAG_SHARE_WECHAT_FRIEND: Toast.makeText(getActivity(), "分享到微信", Toast.LENGTH_SHORT).show(); break; case TAG_SHARE_WECHAT_MOMENT: Toast.makeText(getActivity(), "分享到朋友圈", Toast.LENGTH_SHORT).show(); break; case TAG_SHARE_WEIBO: Toast.makeText(getActivity(), "分享到微博", Toast.LENGTH_SHORT).show(); break; case TAG_SHARE_CHAT: Toast.makeText(getActivity(), "分享到私信", Toast.LENGTH_SHORT).show(); break; case TAG_SHARE_LOCAL: Toast.makeText(getActivity(), "保存到本地", Toast.LENGTH_SHORT).show(); break; } } }).build().show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDButtonFragment.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.qmuidemo.fragment.components import android.view.LayoutInflater import android.view.View import butterknife.BindView import butterknife.ButterKnife import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmui.arch.effect.MapEffect import com.qmuiteam.qmui.kotlin.onClick import com.qmuiteam.qmui.kotlin.skin import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.BaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget import com.qmuiteam.qmuidemo.manager.QDDataManager import com.qmuiteam.qmuidemo.model.QDItemDescription @LatestVisitRecord @Widget(name = "QMUIRoundButton", iconRes = R.mipmap.icon_grid_button) class QDButtonFragment : BaseFragment() { @BindView(R.id.topbar) internal lateinit var mTopBar: QMUITopBarLayout @BindView(R.id.alpha_button) internal lateinit var alphaButton: QMUIRoundButton @BindView(R.id.test_java_kotlin_skin) internal lateinit var kotlinSkinButton: QMUIRoundButton private lateinit var mQDItemDescription: QDItemDescription override fun onCreateView(): View { val view = LayoutInflater.from(activity).inflate(R.layout.fragment_button, null) ButterKnife.bind(this, view) mQDItemDescription = QDDataManager.getInstance().getDescription(this.javaClass) alphaButton.setChangeAlphaWhenPress(true) initTopBar() kotlinSkinButton.skin { border(R.attr.app_skin_btn_test_border_single) background(R.attr.app_skin_btn_test_bg_single) textColor(R.attr.app_skin_btn_test_border_single) } return view } private fun initTopBar() { mTopBar.addLeftBackImageButton().onClick { popBackStack() } mTopBar.setTitle(mQDItemDescription.name) notifyEffect(MapEffect(HashMap().apply { put("interested_type_key", 1) put("interested_value_key", "Did you received the change from other fragment?") })) } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDCollapsingTopBarLayoutFragment.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.fragment.components; import android.animation.ValueAnimator; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.widget.QMUICollapsingTopBarLayout; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-09-02 */ @Widget(widgetClass = QMUICollapsingTopBarLayout.class, iconRes = R.mipmap.icon_grid_collapse_top_bar) public class QDCollapsingTopBarLayoutFragment extends BaseFragment { private static final String TAG = "CollapsingTopBarLayout"; QDRecyclerViewAdapter mRecyclerViewAdapter; LinearLayoutManager mPagerLayoutManager; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; @BindView(R.id.collapsing_topbar_layout) QMUICollapsingTopBarLayout mCollapsingTopBarLayout; @BindView(R.id.topbar) QMUITopBar mTopBar; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getContext()).inflate(R.layout.fragment_collapsing_topbar_layout, null); ButterKnife.bind(this, rootView); initTopBar(); mPagerLayoutManager = new LinearLayoutManager(getContext()); mRecyclerView.setLayoutManager(mPagerLayoutManager); mRecyclerViewAdapter = new QDRecyclerViewAdapter(); mRecyclerViewAdapter.setItemCount(10); mRecyclerView.setAdapter(mRecyclerViewAdapter); mCollapsingTopBarLayout.setScrimUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Log.i(TAG, "scrim: " + animation.getAnimatedValue()); } }); mCollapsingTopBarLayout.addOnOffsetUpdateListener(new QMUICollapsingTopBarLayout.OnOffsetUpdateListener() { @Override public void onOffsetChanged(QMUICollapsingTopBarLayout layout, int offset, float expandFraction) { Log.i(TAG, "offset = " + offset + "; expandFraction = " + expandFraction); } }); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mCollapsingTopBarLayout.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDEmptyViewFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.widget.QMUIEmptyView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIEmptyView} 的使用示例。 * Created by cgspine on 15/9/14. */ @Widget(widgetClass = QMUIEmptyView.class, iconRes = R.mipmap.icon_grid_empty_view) public class QDEmptyViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.emptyView) QMUIEmptyView mEmptyView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_emptyview, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); // 切换其他情况的按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .addItem(getResources().getString(R.string.emptyView_mode_title_double_text)) .addItem(getResources().getString(R.string.emptyView_mode_title_single_text)) .addItem(getResources().getString(R.string.emptyView_mode_title_loading)) .addItem(getResources().getString(R.string.emptyView_mode_title_single_text_and_button)) .addItem(getResources().getString(R.string.emptyView_mode_title_double_text_and_button)) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); switch (position) { case 0: mEmptyView.show(getResources().getString(R.string.emptyView_mode_desc_double), getResources().getString(R.string.emptyView_mode_desc_detail_double)); break; case 1: mEmptyView.show(getResources().getString(R.string.emptyView_mode_desc_single), null); break; case 2: mEmptyView.show(true); break; case 3: mEmptyView.show(false, getResources().getString(R.string.emptyView_mode_desc_fail_title), null, getResources().getString(R.string.emptyView_mode_desc_retry), null); break; case 4: mEmptyView.show(false, getResources().getString(R.string.emptyView_mode_desc_fail_title), getResources().getString(R.string.emptyView_mode_desc_fail_desc), getResources().getString(R.string.emptyView_mode_desc_retry), null); break; default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDFloatLayoutFragment.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.fragment.components; import androidx.core.content.ContextCompat; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUIFloatLayout; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIFloatLayout.class, iconRes = R.mipmap.icon_grid_float_layout) public class QDFloatLayoutFragment extends BaseFragment { private static final String TAG = "QDFloatLayoutFragment"; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.qmuidemo_floatlayout) QMUIFloatLayout mFloatLayout; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_floatlayout, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); for (int i = 0; i < 8; i++) { addItemToFloatLayout(mFloatLayout); } mFloatLayout.setOnLineCountChangeListener(new QMUIFloatLayout.OnLineCountChangeListener() { @Override public void onChange(int oldLineCount, int newLineCount) { Log.i(TAG, "oldLineCount = " + oldLineCount + " ;newLineCount = " + newLineCount); } }); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheet(); } }); } private void addItemToFloatLayout(QMUIFloatLayout floatLayout) { int currentChildCount = floatLayout.getChildCount(); TextView textView = new TextView(getActivity()); int textViewPadding = QMUIDisplayHelper.dp2px(getContext(), 4); textView.setPadding(textViewPadding, textViewPadding, textViewPadding, textViewPadding); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); textView.setTextColor(ContextCompat.getColor(getContext(), R.color.qmui_config_color_white)); textView.setText(String.valueOf(currentChildCount)); textView.setBackgroundResource(currentChildCount % 2 == 0 ? R.color.app_color_theme_3 : R.color.app_color_theme_6); int textViewSize = QMUIDisplayHelper.dpToPx(60); ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(textViewSize, textViewSize); floatLayout.addView(textView, lp); } private void removeItemFromFloatLayout(QMUIFloatLayout floatLayout) { if (floatLayout.getChildCount() == 0) { return; } floatLayout.removeView(floatLayout.getChildAt(floatLayout.getChildCount() - 1)); } private void showBottomSheet() { new QMUIBottomSheet.BottomListSheetBuilder(getContext()) .addItem("增加一个item") .addItem("减少一个item") .addItem("居左") .addItem("居中") .addItem("居右") .addItem("限制最多显示1行") .addItem("限制最多显示4个item") .addItem("不限制行数或个数") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { switch (position) { case 0: addItemToFloatLayout(mFloatLayout); break; case 1: removeItemFromFloatLayout(mFloatLayout); break; case 2: mFloatLayout.setGravity(Gravity.LEFT); break; case 3: mFloatLayout.setGravity(Gravity.CENTER_HORIZONTAL); break; case 4: mFloatLayout.setGravity(Gravity.RIGHT); break; case 5: mFloatLayout.setMaxLines(1); break; case 6: mFloatLayout.setMaxNumber(4); break; case 7: mFloatLayout.setMaxLines(Integer.MAX_VALUE); break; default: break; } dialog.dismiss(); } }) .build().show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDGroupListViewFragment.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.qmuidemo.fragment.components import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.core.content.ContextCompat import butterknife.BindView import butterknife.ButterKnife import com.qmuiteam.qmui.exposure.simpleExposure import com.qmuiteam.qmui.util.QMUIDisplayHelper import com.qmuiteam.qmui.util.QMUIResHelper import com.qmuiteam.qmui.widget.QMUILoadingView import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView import com.qmuiteam.qmui.widget.grouplist.QMUICommonListItemView.SkinConfig import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.BaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget import com.qmuiteam.qmuidemo.manager.QDDataManager import com.qmuiteam.qmuidemo.model.QDItemDescription /** * [QMUIGroupListView] 的使用示例。 * Created by Kayo on 2016/11/21. */ @Widget(widgetClass = QMUIGroupListView::class, iconRes = R.mipmap.icon_grid_group_list_view) class QDGroupListViewFragment : BaseFragment() { @JvmField @BindView(R.id.topbar) var mTopBar: QMUITopBarLayout? = null @JvmField @BindView(R.id.groupListView) var mGroupListView: QMUIGroupListView? = null private var mQDItemDescription: QDItemDescription? = null override fun onCreateView(): View { val root = LayoutInflater.from(activity).inflate(R.layout.fragment_grouplistview, null) ButterKnife.bind(this, root) mQDItemDescription = QDDataManager.getInstance().getDescription(this.javaClass) initTopBar() initGroupListView() return root } private fun initTopBar() { mTopBar!!.addLeftBackImageButton().setOnClickListener { popBackStack() } mTopBar!!.setTitle(mQDItemDescription!!.name) } private fun initGroupListView() { val normalItem = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "Item 1", null, QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_NONE ) normalItem.orientation = QMUICommonListItemView.VERTICAL val itemWithDetail = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.example_image0), "Item 2", null, QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_NONE ) // 去除 icon 的 tintColor 换肤设置 val skinConfig = SkinConfig() skinConfig.iconTintColorRes = 0 itemWithDetail.setSkinConfig(skinConfig) itemWithDetail.detailText = "在右方的详细信息" val itemWithDetailBelow = mGroupListView!!.createItemView("Item 3") itemWithDetailBelow.simpleExposure(key = "") { type -> Log.i("exposure", "simple exposure: $type") } itemWithDetailBelow.orientation = QMUICommonListItemView.VERTICAL itemWithDetailBelow.detailText = "在标题下方的详细信息" val itemWithChevron = mGroupListView!!.createItemView("Item 4") itemWithChevron.accessoryType = QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON val itemWithSwitch = mGroupListView!!.createItemView("Item 5") itemWithSwitch.accessoryType = QMUICommonListItemView.ACCESSORY_TYPE_SWITCH itemWithSwitch.switch.setOnCheckedChangeListener { _, isChecked -> Toast.makeText( activity, "checked = $isChecked", Toast.LENGTH_SHORT ).show() } val itemWithDetailBelowWithChevron = mGroupListView!!.createItemView("Item 6") itemWithDetailBelowWithChevron.orientation = QMUICommonListItemView.VERTICAL itemWithDetailBelowWithChevron.accessoryType = QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON itemWithDetailBelowWithChevron.detailText = "在标题下方的详细信息" val longTitleAndDetail = mGroupListView!!.createItemView( null, "标题有点长;标题有点长;标题有点长;标题有点长;标题有点长;标题有点长", "详细信息有点长; 详细信息有点长;详细信息有点长;详细信息有点长;详细信息有点长", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, ViewGroup.LayoutParams.WRAP_CONTENT ) val paddingVer = QMUIDisplayHelper.dp2px(context, 12) longTitleAndDetail.setPadding( longTitleAndDetail.paddingLeft, paddingVer, longTitleAndDetail.paddingRight, paddingVer ) val height = QMUIResHelper.getAttrDimen(context, com.qmuiteam.qmui.R.attr.qmui_list_item_height) val itemWithDetailBelowWithChevronWithIcon = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "Item 7", "在标题下方的详细信息", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) val itemWithCustom = mGroupListView!!.createItemView("右方自定义 View") itemWithCustom.accessoryType = QMUICommonListItemView.ACCESSORY_TYPE_CUSTOM val loadingView = QMUILoadingView(activity) itemWithCustom.addAccessoryCustomView(loadingView) val itemRedPoint1 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "红点显示在左边", "在标题下方的详细信息", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemRedPoint1.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT) itemRedPoint1.showRedDot(true) val itemRedPoint2 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "红点显示在右边", "在标题下方的详细信息", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemRedPoint2.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT) itemRedPoint2.showRedDot(true) val itemRedPoint3 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "红点显示在左边", "在右方的详细信息", QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemRedPoint3.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT) itemRedPoint3.showRedDot(true) val itemRedPoint4 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "红点显示在右边", "在右方的详细信息", QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemRedPoint4.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT) itemRedPoint4.showRedDot(true) val itemNew1 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "new 标识显示在左边", "在标题下方的详细信息", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemNew1.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT) itemNew1.showNewTip(true) val itemNew2 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "new 标识显示在右边", "在标题下方的详细信息", QMUICommonListItemView.VERTICAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemNew2.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT) itemNew2.showNewTip(true) val itemNew3 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "new 标识显示在左边", "在右方的详细信息", QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemNew3.setTipPosition(QMUICommonListItemView.TIP_POSITION_LEFT) itemNew3.showNewTip(true) val itemNew4 = mGroupListView!!.createItemView( ContextCompat.getDrawable(requireContext(), R.mipmap.about_logo), "new 标识显示在右边", "在右方的详细信息", QMUICommonListItemView.HORIZONTAL, QMUICommonListItemView.ACCESSORY_TYPE_CHEVRON, height ) itemNew4.setTipPosition(QMUICommonListItemView.TIP_POSITION_RIGHT) itemNew4.showNewTip(true) val onClickListener = View.OnClickListener { v -> if (v is QMUICommonListItemView) { val text = v.text Toast.makeText(activity, "$text is Clicked", Toast.LENGTH_SHORT).show() if (v.accessoryType == QMUICommonListItemView.ACCESSORY_TYPE_SWITCH) { v.switch.toggle() } } } val size = QMUIDisplayHelper.dp2px(context, 20) QMUIGroupListView.newSection(context) .setTitle("Section 1: 默认提供的样式") .setDescription("Section 1 的描述") .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT) .addItemView(normalItem, onClickListener) .addItemView(itemWithDetail, onClickListener) .addItemView(itemWithDetailBelow, onClickListener) .addItemView(itemWithChevron, onClickListener) .addItemView(itemWithSwitch, onClickListener) .addItemView(itemWithDetailBelowWithChevron, onClickListener) .addItemView(itemWithDetailBelowWithChevronWithIcon, onClickListener) .addItemView(longTitleAndDetail, onClickListener) .setMiddleSeparatorInset(QMUIDisplayHelper.dp2px(context, 16), 0) .addTo(mGroupListView) QMUIGroupListView.newSection(context) .setTitle("Section 2: 自定义右侧 View/红点/new 提示") .setLeftIconSize(size, ViewGroup.LayoutParams.WRAP_CONTENT) .addItemView(itemWithCustom, onClickListener) .addItemView(itemRedPoint1, onClickListener) .addItemView(itemRedPoint2, onClickListener) .addItemView(itemRedPoint3, onClickListener) .addItemView(itemRedPoint4, onClickListener) .addItemView(itemNew1, onClickListener) .addItemView(itemNew2, onClickListener) .addItemView(itemNew3, onClickListener) .addItemView(itemNew4, onClickListener) .setOnlyShowStartEndSeparator(true) .addTo(mGroupListView) } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDLayoutFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.TextView; import com.qmuiteam.qmui.layout.QMUILayoutHelper; import com.qmuiteam.qmui.layout.QMUILinearLayout; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIWindowHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; /** * Created by cgspine on 2018/3/22. */ @Widget(name = "QMUILayout", iconRes = R.mipmap.icon_grid_layout) public class QDLayoutFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.layout_for_test) QMUILinearLayout mTestLayout; @BindView(R.id.test_seekbar_alpha) SeekBar mAlphaSeekBar; @BindView(R.id.test_seekbar_elevation) SeekBar mElevationSeekBar; @BindView(R.id.alpha_tv) TextView mAlphaTv; @BindView(R.id.elevation_tv) TextView mElevationTv; @BindView(R.id.hide_radius_group) RadioGroup mHideRadiusGroup; private QDItemDescription mQDItemDescription; private float mShadowAlpha = 0.25f; private int mShadowElevationDp = 14; private int mRadius; @OnClick(R.id.shadow_color_red) void changeToShadowColorRed(){ mTestLayout.setShadowColor(0xffff0000); } @OnClick(R.id.shadow_color_blue) void changeToShadowColorBlue(){ mTestLayout.setShadowColor(0xff0000ff); } @OnClick(R.id.radius_15dp) void changeToRadius15dp(){ mRadius = QMUIDisplayHelper.dp2px(getContext(), 15); mTestLayout.setRadius(mRadius); } @OnClick(R.id.radius_half_width) void changeToRadiusHalfWidth(){ mRadius = QMUILayoutHelper.RADIUS_OF_HALF_VIEW_WIDTH; mTestLayout.setRadius(mRadius); } @OnClick(R.id.radius_half_height) void changeToRadiusHalfHeight(){ mRadius = QMUILayoutHelper.RADIUS_OF_HALF_VIEW_HEIGHT; mTestLayout.setRadius(mRadius); } @Override protected View onCreateView() { mRadius = QMUIDisplayHelper.dp2px(getContext(), 15); View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_layout, null); ButterKnife.bind(this, view); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initLayout(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initLayout() { mAlphaSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { mShadowAlpha = progress * 1f / 100; mAlphaTv.setText("alpha: " + mShadowAlpha); mTestLayout.setRadiusAndShadow(mRadius, QMUIDisplayHelper.dp2px(getContext(), mShadowElevationDp), mShadowAlpha); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); mElevationSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { mShadowElevationDp = progress; mElevationTv.setText("elevation: " + progress + "dp"); mTestLayout.setRadiusAndShadow(mRadius, QMUIDisplayHelper.dp2px(getActivity(), mShadowElevationDp), mShadowAlpha); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); mAlphaSeekBar.setProgress((int) (mShadowAlpha * 100)); mElevationSeekBar.setProgress(mShadowElevationDp); mHideRadiusGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.hide_radius_none: mTestLayout.setRadius(mRadius, QMUILayoutHelper.HIDE_RADIUS_SIDE_NONE); break; case R.id.hide_radius_left: mTestLayout.setRadius(mRadius, QMUILayoutHelper.HIDE_RADIUS_SIDE_LEFT); break; case R.id.hide_radius_top: mTestLayout.setRadius(mRadius, QMUILayoutHelper.HIDE_RADIUS_SIDE_TOP); break; case R.id.hide_radius_bottom: mTestLayout.setRadius(mRadius, QMUILayoutHelper.HIDE_RADIUS_SIDE_BOTTOM); break; case R.id.hide_radius_right: mTestLayout.setRadius(mRadius, QMUILayoutHelper.HIDE_RADIUS_SIDE_RIGHT); break; } } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDLinkTextViewFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.textview.QMUILinkTextView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-05-05 */ @Widget(widgetClass = QMUILinkTextView.class, iconRes = R.mipmap.icon_grid_link_text_view) public class QDLinkTextViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.link_text_view) QMUILinkTextView mLinkTextView; @Override protected View onCreateView() { View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_link_texview_layout, null); ButterKnife.bind(this, view); initTopBar(); mLinkTextView.setOnLinkClickListener(mOnLinkClickListener); mLinkTextView.setOnLinkLongClickListener(new QMUILinkTextView.OnLinkLongClickListener() { @Override public void onLongClick(String text) { Toast.makeText(getContext(), "long click: " + text, Toast.LENGTH_SHORT).show(); } }); mLinkTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), "click TextView", Toast.LENGTH_SHORT).show(); } }); // if parent click event should be triggered when TextView area is clicked // mLinkTextView.setNeedForceEventToParent(true); // view.setOnClickListener(new View.OnClickListener() { // @Override // public void onClick(View v) { // Toast.makeText(getContext(), "forceEventToParent", Toast.LENGTH_SHORT).show(); // } // }); return view; } private void initTopBar() { mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); } private QMUILinkTextView.OnLinkClickListener mOnLinkClickListener = new QMUILinkTextView.OnLinkClickListener() { @Override public void onTelLinkClick(String phoneNumber) { Toast.makeText(getContext(), "识别到电话号码是:" + phoneNumber, Toast.LENGTH_SHORT).show(); } @Override public void onMailLinkClick(String mailAddress) { Toast.makeText(getContext(), "识别到邮件地址是:" + mailAddress, Toast.LENGTH_SHORT).show(); } @Override public void onWebUrlLinkClick(String url) { Toast.makeText(getContext(), "识别到网页链接是:" + url, Toast.LENGTH_SHORT).show(); } }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPopupFragment.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.fragment.components; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.PopupWindow; import android.widget.TextView; import android.widget.Toast; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIKeyboardHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.popup.QMUIFullScreenPopup; import com.qmuiteam.qmui.widget.popup.QMUIPopup; import com.qmuiteam.qmui.widget.popup.QMUIPopups; import com.qmuiteam.qmui.widget.popup.QMUIQuickAction; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; /** * @author cginechen * @date 2017-03-27 */ @Widget(widgetClass = QMUIPopups.class, iconRes = R.mipmap.icon_grid_popup) @LatestVisitRecord public class QDPopupFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; private QMUIPopup mNormalPopup; @OnClick(R.id.actionBtn1) void onClickBtn1(View v) { TextView textView = new TextView(getContext()); textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); int padding = QMUIDisplayHelper.dp2px(getContext(), 20); textView.setPadding(padding, padding, padding, padding); textView.setText("QMUIBasePopup 可以设置其位置以及显示和隐藏的动画"); textView.setTextColor( QMUIResHelper.getAttrColor(getContext(), R.attr.app_skin_common_title_text_color)); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.textColor(R.attr.app_skin_common_title_text_color); QMUISkinHelper.setSkinValue(textView, builder); builder.release(); mNormalPopup = QMUIPopups.popup(getContext(), QMUIDisplayHelper.dp2px(getContext(), 250)) .preferredDirection(QMUIPopup.DIRECTION_BOTTOM) .view(textView) .skinManager(QMUISkinManager.defaultInstance(getContext())) .edgeProtection(QMUIDisplayHelper.dp2px(getContext(), 20)) .offsetX(QMUIDisplayHelper.dp2px(getContext(), 20)) .offsetYIfBottom(QMUIDisplayHelper.dp2px(getContext(), 5)) .shadow(true) .arrow(true) .animStyle(QMUIPopup.ANIM_GROW_FROM_CENTER) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); } @OnClick(R.id.actionBtn2) void onClickBtn2(View v) { String[] listItems = new String[]{ "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", }; List data = new ArrayList<>(); Collections.addAll(data, listItems); ArrayAdapter adapter = new ArrayAdapter<>(getContext(), R.layout.simple_list_item, data); AdapterView.OnItemClickListener onItemClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { Toast.makeText(getActivity(), "Item " + (i + 1), Toast.LENGTH_SHORT).show(); if (mNormalPopup != null) { mNormalPopup.dismiss(); } } }; mNormalPopup = QMUIPopups.listPopup(getContext(), QMUIDisplayHelper.dp2px(getContext(), 250), QMUIDisplayHelper.dp2px(getContext(), 300), adapter, onItemClickListener) .animStyle(QMUIPopup.ANIM_GROW_FROM_CENTER) .preferredDirection(QMUIPopup.DIRECTION_TOP) .shadow(true) .offsetYIfTop(QMUIDisplayHelper.dp2px(getContext(), 5)) .skinManager(QMUISkinManager.defaultInstance(getContext())) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); } @OnClick(R.id.actionBtn3) void onClickBtn3(View v) { TextView textView = new TextView(getContext()); textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); int padding = QMUIDisplayHelper.dp2px(getContext(), 20); textView.setPadding(padding, padding, padding, padding); textView.setText("通过 dimAmount() 设置背景遮罩"); textView.setTextColor( QMUIResHelper.getAttrColor(getContext(), R.attr.app_skin_common_title_text_color)); QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); builder.textColor(R.attr.app_skin_common_title_text_color); QMUISkinHelper.setSkinValue(textView, builder); builder.release(); mNormalPopup = QMUIPopups.popup(getContext(), QMUIDisplayHelper.dp2px(getContext(), 250)) .preferredDirection(QMUIPopup.DIRECTION_BOTTOM) .view(textView) .edgeProtection(QMUIDisplayHelper.dp2px(getContext(), 20)) .dimAmount(0.6f) .animStyle(QMUIPopup.ANIM_GROW_FROM_CENTER) .skinManager(QMUISkinManager.defaultInstance(getContext())) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); } @OnClick(R.id.actionBtn4) void onClickBtn4(View v) { final TextView textView = new TextView(getContext()); textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); int padding = QMUIDisplayHelper.dp2px(getContext(), 20); textView.setPadding(padding, padding, padding, padding); textView.setText("加载中..."); textView.setTextColor(ContextCompat.getColor(getContext(), R.color.app_color_description)); mNormalPopup = QMUIPopups.popup(getContext(), QMUIDisplayHelper.dp2px(getContext(), 250)) .preferredDirection(QMUIPopup.DIRECTION_BOTTOM) .view(textView) .edgeProtection(QMUIDisplayHelper.dp2px(getContext(), 20)) .dimAmount(0.6f) .skinManager(QMUISkinManager.defaultInstance(getContext())) .animStyle(QMUIPopup.ANIM_GROW_FROM_CENTER) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); // 这里只是演示,实际情况应该考虑数据加载完成而 Popup 被 dismiss 的情况 textView.postDelayed(new Runnable() { @Override public void run() { textView.setText("使用 Popup 最好是一开始就确定内容宽高," + "如果宽高位置会变化,系统会有一个的移动动画不受控制,体验并不好"); } }, 2000); } @OnClick(R.id.actionBtn5) void onClickBtn5(View v) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); QMUIFrameLayout frameLayout = new QMUIFrameLayout(getContext()); frameLayout.setBackground( QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_skin_support_popup_bg)); builder.background(R.attr.qmui_skin_support_popup_bg); QMUISkinHelper.setSkinValue(frameLayout, builder); frameLayout.setRadius(QMUIDisplayHelper.dp2px(getContext(), 12)); int padding = QMUIDisplayHelper.dp2px(getContext(), 20); frameLayout.setPadding(padding, padding, padding, padding); TextView textView = new TextView(getContext()); textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); textView.setPadding(padding, padding, padding, padding); textView.setText("这是自定义显示的内容"); textView.setTextColor( QMUIResHelper.getAttrColor(getContext(), R.attr.app_skin_common_title_text_color)); builder.clear(); builder.textColor(R.attr.app_skin_common_title_text_color); QMUISkinHelper.setSkinValue(textView, builder); textView.setGravity(Gravity.CENTER); builder.release(); int size = QMUIDisplayHelper.dp2px(getContext(), 200); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size, size); frameLayout.addView(textView, lp); QMUIPopups.fullScreenPopup(getContext()) .addView(frameLayout) .closeBtn(true) .skinManager(QMUISkinManager.defaultInstance(getContext())) .onBlankClick(new QMUIFullScreenPopup.OnBlankClickListener() { @Override public void onBlankClick(QMUIFullScreenPopup popup) { Toast.makeText(getContext(), "点击到空白区域", Toast.LENGTH_SHORT).show(); } }) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); } @OnClick(R.id.actionBtn6) void onClickBtn6(View v) { QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); QMUIFrameLayout frameLayout = new QMUIFrameLayout(getContext()); frameLayout.setBackground( QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_skin_support_popup_bg)); builder.background(R.attr.qmui_skin_support_popup_bg); QMUISkinHelper.setSkinValue(frameLayout, builder); frameLayout.setRadius(QMUIDisplayHelper.dp2px(getContext(), 12)); int padding = QMUIDisplayHelper.dp2px(getContext(), 20); frameLayout.setPadding(padding, padding, padding, padding); QMUIKeyboardHelper.listenKeyBoardWithOffsetSelfHalf(frameLayout, true); TextView textView = new TextView(getContext()); textView.setLineSpacing(QMUIDisplayHelper.dp2px(getContext(), 4), 1.0f); textView.setPadding(padding, padding, padding, padding); textView.setText("这是自定义显示的内容"); builder.clear(); builder.textColor(R.attr.app_skin_common_title_text_color); QMUISkinHelper.setSkinValue(textView, builder); textView.setGravity(Gravity.CENTER); int size = QMUIDisplayHelper.dp2px(getContext(), 200); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size, size); frameLayout.addView(textView, lp); final FrameLayout editFitSystemWindowWrapped = new FrameLayout(getContext()); editFitSystemWindowWrapped.setFitsSystemWindows(true); QMUIWindowInsetHelper.handleWindowInsets(editFitSystemWindowWrapped, WindowInsetsCompat.Type.navigationBars() | WindowInsetsCompat.Type.displayCutout(), true); QMUIKeyboardHelper.listenKeyBoardWithOffsetSelf(editFitSystemWindowWrapped, true); int minHeight = QMUIDisplayHelper.dp2px(getContext(), 48); QMUIFrameLayout editParent = new QMUIFrameLayout(getContext()); editParent.setMinimumHeight(minHeight); editParent.setRadius(minHeight / 2); editParent.setBackground( QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_skin_support_popup_bg)); builder.clear(); builder.background(R.attr.qmui_skin_support_popup_bg); QMUISkinHelper.setSkinValue(editParent, builder); EditText editText = new EditText(getContext()); editText.setHint("请输入..."); editText.setBackground(null); builder.clear(); builder.hintColor(R.attr.app_skin_common_desc_text_color); builder.textColor(R.attr.app_skin_common_title_text_color); QMUISkinHelper.setSkinValue(editText, builder); int paddingHor = QMUIDisplayHelper.dp2px(getContext(), 20); int paddingVer = QMUIDisplayHelper.dp2px(getContext(), 10); editText.setPadding(paddingHor, paddingVer, paddingHor, paddingVer); editText.setMaxHeight(QMUIDisplayHelper.dp2px(getContext(), 100)); FrameLayout.LayoutParams editLp = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); editLp.gravity = Gravity.CENTER_HORIZONTAL; editParent.addView(editText, editLp); editFitSystemWindowWrapped.addView(editParent, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); ConstraintLayout.LayoutParams eLp = new ConstraintLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); int mar = QMUIDisplayHelper.dp2px(getContext(), 20); eLp.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; eLp.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; eLp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; eLp.leftMargin = mar; eLp.rightMargin = mar; eLp.bottomMargin = mar; QMUIPopups.fullScreenPopup(getContext()) .addView(frameLayout) .addView(editFitSystemWindowWrapped, eLp) .skinManager(QMUISkinManager.defaultInstance(getContext())) .onBlankClick(new QMUIFullScreenPopup.OnBlankClickListener() { @Override public void onBlankClick(QMUIFullScreenPopup popup) { popup.dismiss(); Toast.makeText(getContext(), "点击到空白区域", Toast.LENGTH_SHORT).show(); } }) .onDismiss(new PopupWindow.OnDismissListener() { @Override public void onDismiss() { Toast.makeText(getContext(), "onDismiss", Toast.LENGTH_SHORT).show(); } }) .show(v); } @OnClick(R.id.actionBtn7) void onClickBtn7(View v) { QMUIPopups.quickAction(getContext(), QMUIDisplayHelper.dp2px(getContext(), 56), QMUIDisplayHelper.dp2px(getContext(), 56)) .shadow(true) .skinManager(QMUISkinManager.defaultInstance(getContext())) .edgeProtection(QMUIDisplayHelper.dp2px(getContext(), 20)) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_copy).text("复制").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "复制成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_line).text("划线").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "划线成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_share).text("分享").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "分享成功", Toast.LENGTH_SHORT).show(); } } )) .show(v); } @OnClick(R.id.actionBtn8) void onClickBtn8(View v) { QMUIPopups.quickAction(getContext(), QMUIDisplayHelper.dp2px(getContext(), 56), QMUIDisplayHelper.dp2px(getContext(), 56)) .shadow(true) .skinManager(QMUISkinManager.defaultInstance(getContext())) .edgeProtection(QMUIDisplayHelper.dp2px(getContext(), 20)) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_copy).text("复制").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "复制成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_line).text("划线").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "划线成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_share).text("分享").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "分享成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_delete_line).text("删除划线").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "删除划线成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_dict).text("词典").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "打开词典", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_share).text("圈圈").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "查询成功", Toast.LENGTH_SHORT).show(); } } )) .addAction(new QMUIQuickAction.Action().icon(R.drawable.icon_quick_action_dict).text("查询").onClick( new QMUIQuickAction.OnClickListener() { @Override public void onClick(QMUIQuickAction quickAction, QMUIQuickAction.Action action, int position) { quickAction.dismiss(); Toast.makeText(getContext(), "查询成功", Toast.LENGTH_SHORT).show(); } } )) .show(v); } @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_popup, null); ButterKnife.bind(this, root); initTopBar(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPriorityLinearLayoutFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.layout.QMUIPriorityLinearLayout; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIPriorityLinearLayout.class, iconRes = R.mipmap.icon_grid_float_layout) public class QDPriorityLinearLayoutFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; public QDPriorityLinearLayoutFragment() { } @Override protected View onCreateView() { View rootView = LayoutInflater.from(getContext()).inflate( R.layout.fragment_priority_linear_layout, null); ButterKnife.bind(this, rootView); initTopBar(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDProgressBarFragment.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.fragment.components; import android.os.Handler; import android.os.Message; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUIProgressBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.lang.ref.WeakReference; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIProgressBar} 的使用示例。 * Created by cgspine on 15/9/15. */ @Widget(widgetClass = QMUIProgressBar.class, iconRes = R.mipmap.icon_grid_progress_bar) public class QDProgressBarFragment extends BaseFragment { protected static final int STOP = 0x10000; protected static final int NEXT = 0x10001; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.rectProgressBar) QMUIProgressBar mRectProgressBar; @BindView(R.id.circleProgressBar) QMUIProgressBar mCircleProgressBar; @BindView(R.id.startBtn) QMUIRoundButton mStartBtn; @BindView(R.id.backBtn) QMUIRoundButton mBackBtn; int count; private QDItemDescription mQDItemDescription; private ProgressHandler myHandler = new ProgressHandler(); @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_progressbar, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); mRectProgressBar.setQMUIProgressBarTextGenerator(new QMUIProgressBar.QMUIProgressBarTextGenerator() { @Override public String generateText(QMUIProgressBar progressBar, int value, int maxValue) { return value + "/" + maxValue; } }); mCircleProgressBar.setQMUIProgressBarTextGenerator(new QMUIProgressBar.QMUIProgressBarTextGenerator() { @Override public String generateText(QMUIProgressBar progressBar, int value, int maxValue) { return 100 * value / maxValue + "%"; } }); myHandler.setProgressBar(mRectProgressBar, mCircleProgressBar); mStartBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { count = 0; new Thread(new Runnable() { @Override public void run() { for (int i = 0; i <= 100; i++) { try { count = i + 1; if (i == 5) { Message msg = new Message(); msg.what = STOP; myHandler.sendMessage(msg); } else { Message msg = new Message(); msg.what = NEXT; msg.arg1 = count; myHandler.sendMessage(msg); } } catch (Exception e) { e.printStackTrace(); } } } }).start(); } }); mBackBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mRectProgressBar.setProgress(0); mCircleProgressBar.setProgress(0); } }); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private static class ProgressHandler extends Handler { private WeakReference weakRectProgressBar; private WeakReference weakCircleProgressBar; void setProgressBar(QMUIProgressBar rectProgressBar, QMUIProgressBar circleProgressBar) { weakRectProgressBar = new WeakReference<>(rectProgressBar); weakCircleProgressBar = new WeakReference<>(circleProgressBar); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case STOP: break; case NEXT: if (!Thread.currentThread().isInterrupted()) { if (weakRectProgressBar.get() != null && weakCircleProgressBar.get() != null) { weakRectProgressBar.get().setProgress(msg.arg1); weakCircleProgressBar.get().setProgress(msg.arg1); } } } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDPullRefreshFragment.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.qmuidemo.fragment.components import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife import com.qmuiteam.qmui.exposure.Exposure import com.qmuiteam.qmui.exposure.ExposureType import com.qmuiteam.qmui.exposure.bindExposure import com.qmuiteam.qmui.exposure.registerExposure import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet.BottomListSheetBuilder import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUICenterGravityRefreshOffsetCalculator import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIDefaultRefreshOffsetCalculator import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIFollowRefreshOffsetCalculator import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout.OnPullListener import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.BaseFragment import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter import com.qmuiteam.qmuidemo.base.RecyclerViewHolder import com.qmuiteam.qmuidemo.lib.annotation.Widget import com.qmuiteam.qmuidemo.manager.QDDataManager import com.qmuiteam.qmuidemo.model.QDItemDescription import java.util.* class ListItemExposure(val text: String): Exposure { override fun same(data: Exposure): Boolean { return data is ListItemExposure && data.text == text } override fun expose(view: View, type: ExposureType) { Log.i("exposure", "list: $text; $text") } override fun toString(): String { return "ListItemExposure: $text" } } /** * @author cginechen * @date 2016-12-14 */ @Widget(widgetClass = QMUIPullRefreshLayout::class, iconRes = R.mipmap.icon_grid_pull_refresh_layout) class QDPullRefreshFragment : BaseFragment() { @JvmField @BindView(R.id.topbar) var mTopBar: QMUITopBarLayout? = null @JvmField @BindView(R.id.pull_to_refresh) var mPullRefreshLayout: QMUIPullRefreshLayout? = null @JvmField @BindView(R.id.listview) var mListView: RecyclerView? = null private var mAdapter: BaseRecyclerAdapter? = null private var mQDItemDescription: QDItemDescription? = null override fun onCreateView(): View { val root = LayoutInflater.from(activity).inflate(R.layout.fragment_pull_refresh_listview, null) ButterKnife.bind(this, root) val QDDataManager = QDDataManager.getInstance() mQDItemDescription = QDDataManager.getDescription(this.javaClass) initTopBar() initData() return root } private fun initTopBar() { mTopBar!!.addLeftBackImageButton().setOnClickListener { popBackStack() } mTopBar!!.setTitle(mQDItemDescription!!.name) // 切换其他情况的按钮 mTopBar!!.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button).setOnClickListener { showBottomSheetList() } } private fun initData() { mListView!!.layoutManager = object : LinearLayoutManager(context) { override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { return RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } } mAdapter = object : BaseRecyclerAdapter(context, null) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder { return super.onCreateViewHolder(parent, viewType).apply { itemView.registerExposure() } } override fun getItemLayoutId(viewType: Int): Int { return android.R.layout.simple_list_item_1 } override fun bindData(holder: RecyclerViewHolder, position: Int, item: String) { holder.setText(android.R.id.text1, item) holder.itemView.bindExposure(ListItemExposure(item)) } } mAdapter?.setOnItemClickListener(BaseRecyclerAdapter.OnItemClickListener { _, pos -> Toast.makeText( context, "click position=$pos", Toast.LENGTH_SHORT ).show() }) mListView!!.adapter = mAdapter onDataLoaded() mPullRefreshLayout!!.setOnPullListener(object : OnPullListener { override fun onMoveTarget(offset: Int) {} override fun onMoveRefreshView(offset: Int) {} override fun onRefresh() { mPullRefreshLayout!!.postDelayed({ onDataLoaded() // for test exposure count++ val data = when (count) { 1 -> { listOf( "Maintain", "Helps", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion" ) } 2 -> { listOf( "hehe","Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion" ) } else -> { listOf( "xixi","Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion" ) } } mAdapter!!.setData(data) mPullRefreshLayout!!.finishRefresh() }, 2000) } }) } private fun onDataLoaded() { val data = listOf( "Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion" ) mAdapter!!.setData(data) } private var count = 0 private fun showBottomSheetList() { BottomListSheetBuilder(activity) .addItem(resources.getString(R.string.pull_refresh_default_offset_calculator)) .addItem(resources.getString(R.string.pull_refresh_follow_offset_calculator)) .addItem(resources.getString(R.string.pull_refresh_center_gravity_offset_calculator)) .setOnSheetItemClickListener { dialog, _, position, _ -> dialog.dismiss() when (position) { 0 -> mPullRefreshLayout!!.setRefreshOffsetCalculator(QMUIDefaultRefreshOffsetCalculator()) 1 -> mPullRefreshLayout!!.setRefreshOffsetCalculator(QMUIFollowRefreshOffsetCalculator()) 2 -> mPullRefreshLayout!!.setRefreshOffsetCalculator(QMUICenterGravityRefreshOffsetCalculator()) else -> {} } } .build() .show() } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRadiusImageView2ScaleTypeFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import com.qmuiteam.qmui.widget.QMUIRadiusImageView2; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; @Widget(name = "QMUIRadiusImageView2 ScaleType") public class QDRadiusImageView2ScaleTypeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.image_1) QMUIRadiusImageView2 mRadius1ImageView; @BindView(R.id.image_2) QMUIRadiusImageView2 mRadius2ImageView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_radius_imageview2_scale_type, null); ButterKnife.bind(this, root); initTopBar(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); // 动态修改效果按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem("CENTER") .addItem("CENTER_INSIDE") .addItem("CENTER_CROP") .addItem("FIT_CENTER") .addItem("FIT_XY") .addItem("FIT_START") .addItem("FIT_END") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); switch (position) { case 0: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER); break; case 1: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); break; case 2: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); break; case 3: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); break; case 4: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_XY); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_XY); break; case 5: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_START); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_START); break; case 6: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_END); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_END); default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRadiusImageView2UsageFragment.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.fragment.components; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView2; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIRadiusImageView2} 的使用示例。 * Created by cgspine on 15/9/15. */ @Widget(name = "QMUIRadiusImageView2 usage") public class QDRadiusImageView2UsageFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.radiusImageView) QMUIRadiusImageView2 mRadiusImageView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_radius_imageview2, null); ButterKnife.bind(this, root); initTopBar(); reset(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); // 动态修改效果按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); } private void reset() { mRadiusImageView.setBorderColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_border_color)); mRadiusImageView.setBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 2)); mRadiusImageView.setCornerRadius(QMUIDisplayHelper.dp2px(getContext(), 10)); mRadiusImageView.setSelectedMaskColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_selected_mask_color)); mRadiusImageView.setSelectedBorderColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_selected_border_color)); mRadiusImageView.setSelectedBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 3)); mRadiusImageView.setTouchSelectModeEnabled(true); mRadiusImageView.setCircle(false); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem(getResources().getString(R.string.circularImageView_modify_1)) .addItem(getResources().getString(R.string.circularImageView_modify_2)) .addItem(getResources().getString(R.string.circularImageView_modify_3)) .addItem(getResources().getString(R.string.circularImageView_modify_4)) .addItem(getResources().getString(R.string.circularImageView_modify_5)) .addItem(getResources().getString(R.string.circularImageView_modify_6)) .addItem(getResources().getString(R.string.circularImageView_modify_8)) .addItem(getResources().getString(R.string.circularImageView_modify_9)) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); reset(); switch (position) { case 0: mRadiusImageView.setBorderColor(Color.BLACK); mRadiusImageView.setBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 4)); break; case 1: mRadiusImageView.setSelectedBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 6)); mRadiusImageView.setSelectedBorderColor(Color.GREEN); break; case 2: mRadiusImageView.setSelectedMaskColor( ContextCompat.getColor( getContext(), R.color.radiusImageView_selected_mask_color)); break; case 3: if (mRadiusImageView.isSelected()) { mRadiusImageView.setSelected(false); } else { mRadiusImageView.setSelected(true); } break; case 4: mRadiusImageView.setCornerRadius(QMUIDisplayHelper.dp2px(getContext(), 20)); break; case 5: mRadiusImageView.setCircle(true); break; case 6: mRadiusImageView.setTouchSelectModeEnabled(false); default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRadiusImageViewFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUIRadiusImageView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIRadiusImageView.class, iconRes = R.mipmap.icon_grid_radius_image_view) public class QDRadiusImageViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRadiusImageViewUsageFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRadiusImageViewUsageFragment fragment = new QDRadiusImageViewUsageFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRadiusImageView2UsageFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRadiusImageView2UsageFragment fragment = new QDRadiusImageView2UsageFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRadiusImageViewScaleTypeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRadiusImageViewScaleTypeFragment fragment = new QDRadiusImageViewScaleTypeFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRadiusImageView2ScaleTypeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRadiusImageView2ScaleTypeFragment fragment = new QDRadiusImageView2ScaleTypeFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRadiusImageViewScaleTypeFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import com.qmuiteam.qmui.widget.QMUIRadiusImageView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; @Widget(name = "QMUIRadiusImageView ScaleType") public class QDRadiusImageViewScaleTypeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.image_1) QMUIRadiusImageView mRadius1ImageView; @BindView(R.id.image_2) QMUIRadiusImageView mRadius2ImageView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_radius_imageview_scale_type, null); ButterKnife.bind(this, root); initTopBar(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); // 动态修改效果按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem("CENTER") .addItem("CENTER_INSIDE") .addItem("CENTER_CROP") .addItem("FIT_CENTER") .addItem("FIT_XY") .addItem("FIT_START") .addItem("FIT_END") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); switch (position) { case 0: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER); break; case 1: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); break; case 2: mRadius1ImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); mRadius2ImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); break; case 3: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); break; case 4: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_XY); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_XY); break; case 5: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_START); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_START); break; case 6: mRadius1ImageView.setScaleType(ImageView.ScaleType.FIT_END); mRadius2ImageView.setScaleType(ImageView.ScaleType.FIT_END); default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRadiusImageViewUsageFragment.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.fragment.components; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUIRadiusImageView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIRadiusImageView} 的使用示例。 * Created by cgspine on 15/9/15. */ @Widget(name = "QMUIRadiusImageView usage") public class QDRadiusImageViewUsageFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.radiusImageView) QMUIRadiusImageView mRadiusImageView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_radius_imageview, null); ButterKnife.bind(this, root); initTopBar(); reset(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); // 动态修改效果按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); } private void reset() { mRadiusImageView.setBorderColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_border_color)); mRadiusImageView.setBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 2)); mRadiusImageView.setCornerRadius(QMUIDisplayHelper.dp2px(getContext(), 10)); mRadiusImageView.setSelectedMaskColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_selected_mask_color)); mRadiusImageView.setSelectedBorderColor( ContextCompat.getColor(getContext(), R.color.radiusImageView_selected_border_color)); mRadiusImageView.setSelectedBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 3)); mRadiusImageView.setTouchSelectModeEnabled(true); mRadiusImageView.setCircle(false); mRadiusImageView.setOval(false); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem(getResources().getString(R.string.circularImageView_modify_1)) .addItem(getResources().getString(R.string.circularImageView_modify_2)) .addItem(getResources().getString(R.string.circularImageView_modify_3)) .addItem(getResources().getString(R.string.circularImageView_modify_4)) .addItem(getResources().getString(R.string.circularImageView_modify_5)) .addItem(getResources().getString(R.string.circularImageView_modify_6)) .addItem(getResources().getString(R.string.circularImageView_modify_7)) .addItem(getResources().getString(R.string.circularImageView_modify_8)) .addItem(getResources().getString(R.string.circularImageView_modify_9)) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); reset(); switch (position) { case 0: mRadiusImageView.setBorderColor(Color.BLACK); mRadiusImageView.setBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 4)); break; case 1: mRadiusImageView.setSelectedBorderWidth(QMUIDisplayHelper.dp2px(getContext(), 6)); mRadiusImageView.setSelectedBorderColor(Color.GREEN); break; case 2: mRadiusImageView.setSelectedMaskColor( ContextCompat.getColor( getContext(), R.color.radiusImageView_selected_mask_color)); break; case 3: if (mRadiusImageView.isSelected()) { mRadiusImageView.setSelected(false); } else { mRadiusImageView.setSelected(true); } break; case 4: mRadiusImageView.setCornerRadius(QMUIDisplayHelper.dp2px(getContext(), 20)); break; case 5: mRadiusImageView.setCircle(true); break; case 6: mRadiusImageView.setOval(true); case 7: mRadiusImageView.setTouchSelectModeEnabled(false); default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDRecyclerViewDraggableScrollBarFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIDraggableScrollBar; import com.qmuiteam.qmui.recyclerView.QMUIRVDraggableScrollBar; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIRVDraggableScrollBar.class, iconRes = R.mipmap.icon_grid_scroll_animator) public class QDRecyclerViewDraggableScrollBarFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVDraggableScrollBar scrollBar = new QMUIRVDraggableScrollBar(0, 0, 0); scrollBar.setEnableScrollBarFadeInOut(true); scrollBar.attachToRecyclerView(mRecyclerView); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_RIGHT; } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSliderFragment.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.fragment.components; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.qmuiteam.qmui.widget.QMUISeekBar; import com.qmuiteam.qmui.widget.QMUISlider; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUISlider.class, iconRes = R.mipmap.icon_grid_slider) @FragmentScheme( name = "slider", activities = {QDMainActivity.class}, customMatcher = SliderSchemeMatcher.class ) public class QDSliderFragment extends BaseFragment implements QMUISlider.Callback { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.slider) QMUISlider mSlider; @BindView(R.id.seekBar) QMUISeekBar mSeekBar; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_slider, null); ButterKnife.bind(this, view); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); // QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire(); // builder.background(R.attr.qmui_config_color_black); // builder.progressColor(R.attr.qmui_config_color_gray_9); // QMUISkinHelper.setSkinValue(mSlider, builder); // builder.clear(); // builder.background(R.attr.qmui_config_color_blue); // builder.border(R.attr.app_skin_btn_test_border); // mSlider.setThumbSkin(builder); // builder.clear(); mSlider.setCallback(this); mSeekBar.setCallback(this); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } @Override public void onProgressChange(QMUISlider slider, int progress, int tickCount, boolean fromUser) { Log.i("QDSliderFragment", "progress = " + progress + "; fromUser = " + fromUser); } @Override public void onStartMoving(QMUISlider slider, int progress, int tickCount) { } @Override public void onStopMoving(QMUISlider slider, int progress, int tickCount) { } @Override public void onTouchDown(QMUISlider slider, int progress, int tickCount, boolean hitThumb) { } @Override public void onTouchUp(QMUISlider slider, int progress, int tickCount) { } @Override public void onLongTouch(QMUISlider slider, int progress, int tickCount) { } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDSpanTouchFixTextViewFragment.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.fragment.components; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; import com.qmuiteam.qmui.span.QMUITouchableSpan; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @Widget(widgetClass = QMUISpanTouchFixTextView.class, iconRes = R.mipmap.icon_grid_span_touch_fix_text_view) public class QDSpanTouchFixTextViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.sysytem_tv_1) TextView mSystemTv1; @BindView(R.id.sysytem_tv_2) TextView mSystemTv2; @BindView(R.id.touch_fix_tv_1) QMUISpanTouchFixTextView mSpanTouchFixTextView1; @BindView(R.id.touch_fix_tv_2) QMUISpanTouchFixTextView mSpanTouchFixTextView2; private int highlightTextNormalColor; private int highlightTextPressedColor; private int highlightBgNormalColor; private int highlightBgPressedColor; @OnClick({R.id.touch_fix_tv_1, R.id.sysytem_tv_1}) void onClickTv(View v) { Toast.makeText(getContext(), "onClickTv", Toast.LENGTH_SHORT).show(); } @OnClick({R.id.click_area_1, R.id.click_area_2}) void onClickArea() { Toast.makeText(getContext(), "onClickArea", Toast.LENGTH_SHORT).show(); } @Override protected View onCreateView() { highlightTextNormalColor = ContextCompat.getColor(getContext(), R.color.app_color_blue_2); highlightTextPressedColor = ContextCompat.getColor(getContext(), R.color.app_color_blue_2_pressed); highlightBgNormalColor = QMUIResHelper.getAttrColor(getContext(), R.attr.qmui_config_color_gray_8); highlightBgPressedColor = QMUIResHelper.getAttrColor(getContext(), R.attr.qmui_config_color_gray_6); View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_touch_span_fix_layout, null); ButterKnife.bind(this, view); initTopBar(); // 场景一 mSystemTv1.setMovementMethod(LinkMovementMethod.getInstance()); mSystemTv1.setText(generateSp(mSystemTv1, getResources().getString(R.string.system_behavior_1))); mSpanTouchFixTextView1.setMovementMethodDefault(); mSpanTouchFixTextView1.setText(generateSp(mSystemTv1, getResources().getString(R.string.span_touch_fix_1))); // 场景二 mSystemTv2.setMovementMethod(LinkMovementMethod.getInstance()); mSystemTv2.setText(generateSp(mSystemTv1, getResources().getString(R.string.system_behavior_2))); mSpanTouchFixTextView2.setMovementMethodDefault(); mSpanTouchFixTextView2.setNeedForceEventToParent(true); mSpanTouchFixTextView2.setText(generateSp(mSpanTouchFixTextView2, getResources().getString(R.string.span_touch_fix_2))); return view; } private SpannableString generateSp(TextView tv, String text) { String highlight1 = "@qmui"; String highlight2 = "#qmui#"; SpannableString sp = new SpannableString(text); int start = 0, end; int index; while ((index = text.indexOf(highlight1, start)) > -1) { end = index + highlight1.length(); sp.setSpan(new QMUITouchableSpan(tv, R.attr.app_skin_span_normal_text_color, R.attr.app_skin_span_pressed_text_color, R.attr.app_skin_span_normal_bg_color, R.attr.app_skin_span_pressed_bg_color) { @Override public void onSpanClick(View widget) { Toast.makeText(getContext(), "click @qmui", Toast.LENGTH_SHORT).show(); } }, index, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); start = end; } start = 0; while ((index = text.indexOf(highlight2, start)) > -1) { end = index + highlight2.length(); sp.setSpan(new QMUITouchableSpan(tv, R.attr.app_skin_span_normal_text_color, R.attr.app_skin_span_pressed_text_color, R.attr.app_skin_span_normal_bg_color, R.attr.app_skin_span_pressed_bg_color) { @Override public void onSpanClick(View widget) { Toast.makeText(getContext(), "click #qmui#", Toast.LENGTH_SHORT).show(); } }, index, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); start = end; } return sp; } private void initTopBar() { mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2FixModeFragment.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.fragment.components; import android.content.Context; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import androidx.core.content.ContextCompat; import androidx.viewpager2.widget.ViewPager2; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmui.widget.tab.QMUITab; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabIndicator; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmui.widget.tab.QMUITabSegment2; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.HashMap; import java.util.Map; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "ViewPager2: 固定宽度,内容均分") public class QDTabSegment2FixModeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment2 mTabSegment; @BindView(R.id.contentViewPager) ViewPager2 mContentViewPager; private ContentPage mDestPage = ContentPage.Item1; private QDItemDescription mQDItemDescription; private QDRecyclerViewAdapter mPagerAdapter; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager2_layout, null); ButterKnife.bind(this, rootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBottomSheetList(); } }); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem(getResources().getString(R.string.tabSegment_mode_general)) .addItem(getResources().getString(R.string.tabSegment_mode_bottom_indicator)) .addItem(getResources().getString(R.string.tabSegment_mode_top_indicator)) .addItem(getResources().getString(R.string.tabSegment_mode_indicator_with_content)) .addItem(getResources().getString(R.string.tabSegment_mode_left_icon_and_auto_tint)) .addItem(getResources().getString(R.string.tabSegment_mode_sign_count)) .addItem(getResources().getString(R.string.tabSegment_mode_icon_change)) .addItem(getResources().getString(R.string.tabSegment_mode_muti_color)) .addItem(getResources().getString(R.string.tabSegment_mode_change_content_by_index)) .addItem(getResources().getString(R.string.tabSegment_mode_replace_tab_by_index)) .addItem(getResources().getString(R.string.tabSegment_mode_scale_selected)) .addItem(getResources().getString(R.string.tabSegment_mode_change_gravity)) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); Context context = getContext(); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder() .setGravity(Gravity.CENTER); int indicatorHeight = QMUIDisplayHelper.dp2px(context, 2); switch (position) { case 0: mTabSegment.reset(); mTabSegment.setIndicator(null); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 1: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 2: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, true, true)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 3: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, false)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 4: { mTabSegment.reset(); mTabSegment.setIndicator(null); tabBuilder.setDynamicChangeIconColor(true); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 5: // mTabSegment.showSignCountView(getContext(), 0, 20); // 也可以直接调用这个 QMUITab tab = mTabSegment.getTab(0); tab.setSignCount(20); QMUITab tab1 = mTabSegment.getTab(1); tab1.setRedPoint(); break; case 6: { mTabSegment.reset(); mTabSegment.setIndicator(null); tabBuilder.setDynamicChangeIconColor(false); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 7: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_blue) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 8: mTabSegment.updateTabText(0, "动态更新文案"); break; case 9: { QMUITab newTab = tabBuilder.setText("动态更新") .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component)) .setDynamicChangeIconColor(true) .build(getContext()); mTabSegment.replaceTab(0, newTab); break; } case 10: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true) .setTextSize( QMUIDisplayHelper.sp2px(context, 13), QMUIDisplayHelper.sp2px(context, 15)) .setSelectedIconScale(1.5f); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_blue, R.attr.qmui_config_color_red) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 11: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true) .setTextSize( QMUIDisplayHelper.sp2px(context, 13), QMUIDisplayHelper.sp2px(context, 15)) .setSelectedIconScale(1.5f) .setGravity(Gravity.LEFT | Gravity.BOTTOM); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_blue, R.attr.qmui_config_color_red) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } default: break; } mTabSegment.notifyDataChanged(); } }) .build() .show(); } private void initTabAndPager() { mPagerAdapter = new QDRecyclerViewAdapter(); mPagerAdapter.setItemCount(ContentPage.SIZE); mContentViewPager.setAdapter(mPagerAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); QMUITabBuilder builder = mTabSegment.tabBuilder(); mTabSegment.addTab(builder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(builder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); mTabSegment.notifyDataChanged(); mTabSegment.setMode(QMUITabSegment.MODE_FIXED); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { } @Override public void onTabUnselected(int index) { } @Override public void onTabReselected(int index) { } @Override public void onDoubleTap(int index) { mTabSegment.clearSignCountView(index); } }); mTabSegment.setupWithViewPager(mContentViewPager); } public enum ContentPage { Item1(0), Item2(1); public static final int SIZE = 2; private final int position; ContentPage(int pos) { position = pos; } public static ContentPage getPage(int position) { switch (position) { case 0: return Item1; case 1: return Item2; default: return Item1; } } public int getPosition() { return position; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegment2ScrollableModeFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import androidx.viewpager2.widget.ViewPager2; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabIndicator; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmui.widget.tab.QMUITabSegment2; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "ViewPager2: 内容自适应,超过父容器则滚动") public class QDTabSegment2ScrollableModeFragment extends BaseFragment { @SuppressWarnings("FieldCanBeLocal") private final int TAB_COUNT = 10; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment2 mTabSegment; @BindView(R.id.contentViewPager) ViewPager2 mContentViewPager; private ContentPage mDestPage = ContentPage.Item1; private QDItemDescription mQDItemDescription; private int mCurrentItemCount = TAB_COUNT; private QDRecyclerViewAdapter mPageAdapter; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager2_layout, null); ButterKnife.bind(this, rootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); mTopBar.addRightTextButton("reduce tab", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { reduceTabCount(); } }); } private void initTabAndPager() { mPageAdapter = new QDRecyclerViewAdapter(); mPageAdapter.setItemCount(mCurrentItemCount); mContentViewPager.setAdapter(mPageAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder(); for (int i = 0; i < mCurrentItemCount; i++) { mTabSegment.addTab(tabBuilder.setText("Item " + (i + 1)).build(getContext())); } int space = QMUIDisplayHelper.dp2px(getContext(), 16); mTabSegment.setIndicator(new QMUITabIndicator( QMUIDisplayHelper.dp2px(getContext(), 2), false, true)); mTabSegment.setMode(QMUITabSegment.MODE_SCROLLABLE); mTabSegment.setItemSpaceInScrollMode(space); mTabSegment.setupWithViewPager(mContentViewPager); mTabSegment.setPadding(space, 0, space, 0); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { Toast.makeText(getContext(), "select index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabUnselected(int index) { Toast.makeText(getContext(), "unSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabReselected(int index) { Toast.makeText(getContext(), "reSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onDoubleTap(int index) { Toast.makeText(getContext(), "double tap index " + index, Toast.LENGTH_SHORT).show(); } }); } private void reduceTabCount() { if (mCurrentItemCount <= 1) { Toast.makeText(getContext(), "Only the last one, don't reduce it anymore!!!", Toast.LENGTH_SHORT).show(); return; } mCurrentItemCount--; mPageAdapter.setItemCount(mCurrentItemCount); mPageAdapter.notifyDataSetChanged(); mTabSegment.reset(); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder(); for (int i = 0; i < mCurrentItemCount; i++) { mTabSegment.addTab(tabBuilder.setText("Item " + (i + 1)).build(getContext())); } mTabSegment.notifyDataChanged(); } public enum ContentPage { Item1(0), Item2(1), Item3(2), Item4(3), Item5(4), Item6(5), Item7(6), Item8(7), Item9(8), Item10(9); private final int position; ContentPage(int pos) { position = pos; } public static ContentPage getPage(int position) { switch (position) { case 0: return Item1; case 1: return Item2; case 2: return Item3; case 3: return Item4; case 4: return Item5; case 5: return Item6; case 6: return Item7; case 7: return Item8; case 8: return Item9; case 9: return Item10; default: return Item1; } } public int getPosition() { return position; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentFixModeFragment.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.fragment.components; import android.content.Context; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmui.widget.tab.QMUITab; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabIndicator; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.HashMap; import java.util.Map; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-04-28 */ @LatestVisitRecord @Widget(group = Group.Other, name = "固定宽度,内容均分") @FragmentScheme( name = "tab", activities = {QDMainActivity.class}, required = {"mode=1"}, keysWithIntValue = {"mode"}) public class QDTabSegmentFixModeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment mTabSegment; @BindView(R.id.contentViewPager) ViewPager mContentViewPager; private Map mPageMap = new HashMap<>(); private ContentPage mDestPage = ContentPage.Item1; private QDItemDescription mQDItemDescription; private PagerAdapter mPagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public int getCount() { return ContentPage.SIZE; } @Override public Object instantiateItem(final ViewGroup container, int position) { ContentPage page = ContentPage.getPage(position); View view = getPageView(page); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.addView(view, params); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } }; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager_layout, null); ButterKnife.bind(this, rootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBottomSheetList(); } }); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem(getResources().getString(R.string.tabSegment_mode_general)) .addItem(getResources().getString(R.string.tabSegment_mode_bottom_indicator)) .addItem(getResources().getString(R.string.tabSegment_mode_top_indicator)) .addItem(getResources().getString(R.string.tabSegment_mode_indicator_with_content)) .addItem(getResources().getString(R.string.tabSegment_mode_left_icon_and_auto_tint)) .addItem(getResources().getString(R.string.tabSegment_mode_sign_count)) .addItem(getResources().getString(R.string.tabSegment_mode_icon_change)) .addItem(getResources().getString(R.string.tabSegment_mode_muti_color)) .addItem(getResources().getString(R.string.tabSegment_mode_change_content_by_index)) .addItem(getResources().getString(R.string.tabSegment_mode_replace_tab_by_index)) .addItem(getResources().getString(R.string.tabSegment_mode_scale_selected)) .addItem(getResources().getString(R.string.tabSegment_mode_change_gravity)) .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); Context context = getContext(); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder() .setGravity(Gravity.CENTER); int indicatorHeight = QMUIDisplayHelper.dp2px(context, 2); switch (position) { case 0: mTabSegment.reset(); mTabSegment.setIndicator(null); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 1: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 2: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, true, true)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 3: mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, false)); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(tabBuilder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); break; case 4: { mTabSegment.reset(); mTabSegment.setIndicator(null); tabBuilder.setDynamicChangeIconColor(true); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 5: // mTabSegment.showSignCountView(getContext(), 0, 20); // 也可以直接调用这个 QMUITab tab = mTabSegment.getTab(0); tab.setSignCount(20); QMUITab tab1 = mTabSegment.getTab(1); tab1.setRedPoint(); break; case 6: { mTabSegment.reset(); mTabSegment.setIndicator(null); tabBuilder.setDynamicChangeIconColor(false); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 7: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_blue) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 8: mTabSegment.updateTabText(0, "动态更新文案"); break; case 9: { QMUITab newTab = tabBuilder.setText("动态更新") .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component)) .setDynamicChangeIconColor(true) .build(getContext()); mTabSegment.replaceTab(0, newTab); break; } case 10: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true) .setTextSize( QMUIDisplayHelper.sp2px(context, 13), QMUIDisplayHelper.sp2px(context, 15)) .setSelectedIconScale(1.5f); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_blue, R.attr.qmui_config_color_red) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } case 11: { mTabSegment.reset(); mTabSegment.setIndicator(new QMUITabIndicator( indicatorHeight, false, true)); tabBuilder.setDynamicChangeIconColor(true) .setTextSize( QMUIDisplayHelper.sp2px(context, 13), QMUIDisplayHelper.sp2px(context, 15)) .setSelectedIconScale(1.5f) .setGravity(Gravity.LEFT | Gravity.BOTTOM); QMUITab component = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_component_selected)) .setText("Components") .setColorAttr(R.attr.qmui_config_color_blue, R.attr.qmui_config_color_red) .build(getContext()); QMUITab util = tabBuilder .setNormalDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(context, R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .setColorAttr(R.attr.qmui_config_color_gray_1, R.attr.qmui_config_color_red) .build(getContext()); mTabSegment.addTab(component); mTabSegment.addTab(util); break; } default: break; } mTabSegment.notifyDataChanged(); } }) .build() .show(); } private void initTabAndPager() { mContentViewPager.setAdapter(mPagerAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); QMUITabBuilder builder = mTabSegment.tabBuilder(); mTabSegment.addTab(builder.setText(getString(R.string.tabSegment_item_1_title)).build(getContext())); mTabSegment.addTab(builder.setText(getString(R.string.tabSegment_item_2_title)).build(getContext())); mTabSegment.setupWithViewPager(mContentViewPager, false); mTabSegment.setMode(QMUITabSegment.MODE_FIXED); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { } @Override public void onTabUnselected(int index) { } @Override public void onTabReselected(int index) { } @Override public void onDoubleTap(int index) { mTabSegment.clearSignCountView(index); } }); } private View getPageView(ContentPage page) { View view = mPageMap.get(page); if (view == null) { TextView textView = new TextView(getContext()); textView.setGravity(Gravity.CENTER); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); textView.setTextColor(ContextCompat.getColor(getContext(), R.color.app_color_description)); if (page == ContentPage.Item1) { textView.setText(R.string.tabSegment_item_1_content); } else if (page == ContentPage.Item2) { textView.setText(R.string.tabSegment_item_2_content); } view = textView; mPageMap.put(page, view); } return view; } public enum ContentPage { Item1(0), Item2(1); public static final int SIZE = 2; private final int position; ContentPage(int pos) { position = pos; } public static ContentPage getPage(int position) { switch (position) { case 0: return Item1; case 1: return Item2; default: return Item1; } } public int getPosition() { return position; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2016-10-21 */ @FragmentScheme(name = "tab", activities = {QDMainActivity.class}) @Widget(widgetClass = QMUITabSegment.class, iconRes = R.mipmap.icon_grid_tab_segment) public class QDTabSegmentFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDTabSegmentFixModeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDTabSegmentFixModeFragment fragment = new QDTabSegmentFixModeFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDTabSegmentScrollableModeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDTabSegmentScrollableModeFragment fragment = new QDTabSegmentScrollableModeFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDTabSegmentSpaceWeightFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDTabSegmentSpaceWeightFragment fragment = new QDTabSegmentSpaceWeightFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDTabSegment2FixModeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDTabSegment2FixModeFragment fragment = new QDTabSegment2FixModeFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDTabSegment2ScrollableModeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDTabSegment2ScrollableModeFragment fragment = new QDTabSegment2ScrollableModeFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentScrollableModeFragment.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.fragment.components; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import android.os.Bundle; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.qmuiteam.qmui.skin.QMUISkinHelper; import com.qmuiteam.qmui.skin.QMUISkinValueBuilder; import com.qmuiteam.qmui.skin.SkinWriter; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabIndicator; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.manager.QDSchemeManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.HashMap; import java.util.Map; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "内容自适应,超过父容器则滚动") @FragmentScheme( name = "tab", useRefreshIfCurrentMatched = true, activities = {QDMainActivity.class}, required = {"mode=2", "name"}, keysWithIntValue = {"mode"}) public class QDTabSegmentScrollableModeFragment extends BaseFragment { @SuppressWarnings("FieldCanBeLocal") private final int TAB_COUNT = 10; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment mTabSegment; @BindView(R.id.contentViewPager) ViewPager mContentViewPager; private Map mPageMap = new HashMap<>(); private ContentPage mDestPage = ContentPage.Item1; private QDItemDescription mQDItemDescription; private int mCurrentItemCount = TAB_COUNT; private PagerAdapter mPagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public int getCount() { return mCurrentItemCount; } @Override public Object instantiateItem(final ViewGroup container, int position) { ContentPage page = ContentPage.getPage(position); View view = getPageView(page); view.setTag(page); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.addView(view, params); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public int getItemPosition(@NonNull Object object) { View view = (View) object; Object page = view.getTag(); if (page instanceof ContentPage) { int pos = ((ContentPage) page).getPosition(); if (pos >= mCurrentItemCount) { return POSITION_NONE; } return POSITION_UNCHANGED; } return POSITION_NONE; } }; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(isStartedByScheme()){ Toast.makeText(getContext(), "started by scheme", Toast.LENGTH_SHORT).show(); Bundle args = getArguments(); if(args != null){ int mode = args.getInt("mode"); String name = args.getString("name"); Toast.makeText(getContext(), "mode = " + mode + "; name = " + name, Toast.LENGTH_SHORT).show(); } } } @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager_layout, null); ButterKnife.bind(this, rootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); mTopBar.addRightTextButton("reduce tab", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { reduceTabCount(); } }); } private void initTabAndPager() { mContentViewPager.setAdapter(mPagerAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder(); for (int i = 0; i < mCurrentItemCount; i++) { mTabSegment.addTab(tabBuilder.setText("Item " + (i + 1)).build(getContext())); } int space = QMUIDisplayHelper.dp2px(getContext(), 16); mTabSegment.setIndicator(new QMUITabIndicator( QMUIDisplayHelper.dp2px(getContext(), 2), false, true)); mTabSegment.setMode(QMUITabSegment.MODE_SCROLLABLE); mTabSegment.setItemSpaceInScrollMode(space); mTabSegment.setupWithViewPager(mContentViewPager, false); mTabSegment.setPadding(space, 0, space, 0); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { Toast.makeText(getContext(), "select index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabUnselected(int index) { Toast.makeText(getContext(), "unSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabReselected(int index) { Toast.makeText(getContext(), "reSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onDoubleTap(int index) { Toast.makeText(getContext(), "double tap index " + index, Toast.LENGTH_SHORT).show(); } }); } private void reduceTabCount() { if (mCurrentItemCount <= 1) { Toast.makeText(getContext(), "Only the last one, don't reduce it anymore!!!", Toast.LENGTH_SHORT).show(); return; } mCurrentItemCount--; mPagerAdapter.notifyDataSetChanged(); mTabSegment.reset(); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder(); for (int i = 0; i < mCurrentItemCount; i++) { mTabSegment.addTab(tabBuilder.setText("Item " + (i + 1)).build(getContext())); } mTabSegment.notifyDataChanged(); } private View getPageView(ContentPage page) { View view = mPageMap.get(page); if (view == null) { TextView textView = new TextView(getContext()); textView.setGravity(Gravity.CENTER); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); textView.setTextColor(ContextCompat.getColor(getContext(), R.color.app_color_description)); textView.setText("这是第 " + (page.getPosition() + 1) + " 个 Item 的内容区"); QMUISkinHelper.setSkinValue(textView, new SkinWriter(){ @Override public void write(QMUISkinValueBuilder builder) { builder.textColor(R.attr.app_skin_common_desc_text_color); } }); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QDSchemeManager.getInstance().handle("qmui://tab?mode=2&name=xixi"); } }); view = textView; mPageMap.put(page, view); } return view; } public enum ContentPage { Item1(0), Item2(1), Item3(2), Item4(3), Item5(4), Item6(5), Item7(6), Item8(7), Item9(8), Item10(9); private final int position; ContentPage(int pos) { position = pos; } public static ContentPage getPage(int position) { switch (position) { case 0: return Item1; case 1: return Item2; case 2: return Item3; case 3: return Item4; case 4: return Item5; case 5: return Item6; case 6: return Item7; case 7: return Item8; case 8: return Item9; case 9: return Item10; default: return Item1; } } public int getPosition() { return position; } } @Override public void refreshFromScheme(@Nullable Bundle bundle) { Toast.makeText(getContext(), "refreshFromScheme: name = " + bundle.getString("name"), Toast.LENGTH_SHORT).show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTabSegmentSpaceWeightFragment.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.fragment.components; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.qmuiteam.qmui.arch.annotation.FragmentScheme; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.tab.QMUITab; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabIndicator; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.HashMap; import java.util.Map; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-04-28 */ @Widget(group = Group.Other, name = "内容自适应,添加weight控制间距") @FragmentScheme( name = "tab", activities = {QDMainActivity.class}, required = {"mode=3"}, keysWithIntValue = {"mode"}) public class QDTabSegmentSpaceWeightFragment extends BaseFragment { @SuppressWarnings("FieldCanBeLocal") private final int TAB_COUNT = 3; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment mTabSegment; @BindView(R.id.contentViewPager) ViewPager mContentViewPager; private Map mPageMap = new HashMap<>(); private ContentPage mDestPage = ContentPage.Item1; private QDItemDescription mQDItemDescription; private int mCurrentItemCount = TAB_COUNT; private PagerAdapter mPagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public int getCount() { return mCurrentItemCount; } @Override public Object instantiateItem(final ViewGroup container, int position) { ContentPage page = ContentPage.getPage(position); View view = getPageView(page); view.setTag(page); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.addView(view, params); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public int getItemPosition(@NonNull Object object) { View view = (View) object; Object page = view.getTag(); if (page instanceof ContentPage) { int pos = ((ContentPage) page).getPosition(); if (pos >= mCurrentItemCount) { return POSITION_NONE; } return POSITION_UNCHANGED; } return POSITION_NONE; } }; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager_layout, null); ButterKnife.bind(this, rootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initTabAndPager() { mContentViewPager.setAdapter(mPagerAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); QMUITabBuilder tabBuilder = mTabSegment.tabBuilder(); for (int i = 0; i < mCurrentItemCount; i++) { QMUITab tab = tabBuilder.setText("Item " + i).build(getContext()); if (i == 0) { tab.setSpaceWeight(0f, 1f); } mTabSegment.addTab(tab); } int space = QMUIDisplayHelper.dp2px(getContext(), 16); mTabSegment.setIndicator( new QMUITabIndicator(QMUIDisplayHelper.dp2px(getContext(), 2), false, true)); mTabSegment.setMode(QMUITabSegment.MODE_SCROLLABLE); mTabSegment.setItemSpaceInScrollMode(space); mTabSegment.setupWithViewPager(mContentViewPager, false); mTabSegment.setPadding(space, 0, space, 0); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { Toast.makeText(getContext(), "select index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabUnselected(int index) { Toast.makeText(getContext(), "unSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onTabReselected(int index) { Toast.makeText(getContext(), "reSelect index " + index, Toast.LENGTH_SHORT).show(); } @Override public void onDoubleTap(int index) { Toast.makeText(getContext(), "double tap index " + index, Toast.LENGTH_SHORT).show(); } }); } private View getPageView(ContentPage page) { View view = mPageMap.get(page); if (view == null) { TextView textView = new TextView(getContext()); textView.setGravity(Gravity.CENTER); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); textView.setTextColor(ContextCompat.getColor(getContext(), R.color.app_color_description)); textView.setText("这是第 " + (page.getPosition() + 1) + " 个 Item 的内容区"); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startFragment(new QDTabSegmentSpaceWeightFragment()); } }); view = textView; mPageMap.put(page, view); } return view; } public enum ContentPage { Item1(0), Item2(1), Item3(2), ; private final int position; ContentPage(int pos) { position = pos; } public static ContentPage getPage(int position) { switch (position) { case 0: return Item1; case 1: return Item2; case 2: return Item3; default: return Item1; } } public int getPosition() { return position; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDTipDialogFragment.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.fragment.components; import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUITipDialog; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUITipDialog} 的使用示例。 * Created by Kayo on 2016/11/21. */ @Widget(widgetClass = QMUITipDialog.class, iconRes = R.mipmap.icon_grid_tip_dialog) public class QDTipDialogFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.listview) ListView mListView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_listview, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initListView() { String[] listItems = new String[]{ "Loading 类型提示框", "成功提示类型提示框", "失败提示类型提示框", "信息提示类型提示框", "单独图片类型提示框", "单独文字类型提示框", "自定义内容提示框" }; List data = new ArrayList<>(); Collections.addAll(data, listItems); mListView.setAdapter(new ArrayAdapter<>(getActivity(), R.layout.simple_list_item, data)); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final QMUITipDialog tipDialog; switch (position) { case 1: tipDialog = new QMUITipDialog.Builder(getContext()) .setIconType(QMUITipDialog.Builder.ICON_TYPE_SUCCESS) .setTipWord("发送成功") .create(); break; case 2: tipDialog = new QMUITipDialog.Builder(getContext()) .setIconType(QMUITipDialog.Builder.ICON_TYPE_FAIL) .setTipWord("发送失败") .create(); break; case 3: tipDialog = new QMUITipDialog.Builder(getContext()) .setIconType(QMUITipDialog.Builder.ICON_TYPE_INFO) .setTipWord("请勿重复操作") .create(); break; case 4: tipDialog = new QMUITipDialog.Builder(getContext()) .setIconType(QMUITipDialog.Builder.ICON_TYPE_SUCCESS) .create(); break; case 5: tipDialog = new QMUITipDialog.Builder(getContext()) .setTipWord("请勿重复操作") .create(); break; case 6: tipDialog = new QMUITipDialog.CustomBuilder(getContext()) .setContent(R.layout.tipdialog_custom) .create(); break; default: tipDialog = new QMUITipDialog.Builder(getContext()) .setIconType(QMUITipDialog.Builder.ICON_TYPE_LOADING) .setTipWord("正在加载") .create(); } tipDialog.show(); mListView.postDelayed(new Runnable() { @Override public void run() { tipDialog.dismiss(); } }, 1500); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/QDVerticalTextViewFragment.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.fragment.components; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import com.qmuiteam.qmui.util.QMUIKeyboardHelper; import com.qmuiteam.qmui.util.QMUILangHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.QMUIVerticalTextView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIVerticalTextView.class, iconRes = R.mipmap.icon_grid_vertical_text_view) public class QDVerticalTextViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.verticalTextView) QMUIVerticalTextView mVerticalTextView; @BindView(R.id.verticalTextView_editText) EditText mEditText; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_verticaltextview, null); ButterKnife.bind(this, rootView); initTopBar(); initVerticalTextView(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); QDItemDescription qdItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); mTopBar.setTitle(qdItemDescription.getName()); } private void initVerticalTextView() { final String defaultText = String.format("%s 实现对文字的垂直排版。并且对非 CJK (中文、日文、韩文)字符做90度旋转排版。可以在下方的输入框中输入文字,体验不同文字垂直排版的效果。", QMUIVerticalTextView.class.getSimpleName()); mVerticalTextView.setText(defaultText); mEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { mVerticalTextView.setText(QMUILangHelper.isNullOrEmpty(s) ? defaultText : s); } }); } @Override protected void popBackStack() { super.popBackStack(); QMUIKeyboardHelper.hideKeyboard(mEditText); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/SliderSchemeMatcher.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.fragment.components; import androidx.annotation.Nullable; import com.qmuiteam.qmui.arch.scheme.QMUIDefaultSchemeMatcher; import com.qmuiteam.qmui.arch.scheme.SchemeItem; import java.util.Map; public class SliderSchemeMatcher extends QMUIDefaultSchemeMatcher { @Override public boolean match(SchemeItem schemeItem, @Nullable Map params) { if (params != null) { try { String modeStr = params.get("mode"); if (modeStr != null && !modeStr.isEmpty()) { int mode = Integer.parseInt(modeStr); return mode > 4; } } catch (Throwable ignore) { } } return false; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/pullLayout/QDPullFragment.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.fragment.components.pullLayout; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIPullLayout.class, iconRes = R.mipmap.icon_grid_pull_layout) public class QDPullFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); injectDocToTopBar(mTopBar); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDPullVerticalTestFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDPullVerticalTestFragment fragment = new QDPullVerticalTestFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDPullHorizontalTestFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDPullHorizontalTestFragment fragment = new QDPullHorizontalTestFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDPullRefreshAndLoadMoreTestFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDPullRefreshAndLoadMoreTestFragment fragment = new QDPullRefreshAndLoadMoreTestFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/pullLayout/QDPullHorizontalTestFragment.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.fragment.components.pullLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.PagerSnapHelper; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @LatestVisitRecord @Widget(group = Group.Other, name = "PullLayout: Horizontal Test") public class QDPullHorizontalTestFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private QDRecyclerViewAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_horizontal_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { mPullLayout.finishActionRun(pullAction); } }, 1000); } }); LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false); mRecyclerView.setLayoutManager(layoutManager); new PagerSnapHelper().attachToRecyclerView(mRecyclerView); mAdapter = new QDRecyclerViewAdapter(); mAdapter.setItemCount(10); mRecyclerView.setAdapter(mAdapter); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/pullLayout/QDPullRefreshAndLoadMoreTestFragment.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.fragment.components.pullLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @LatestVisitRecord @Widget(group = Group.Other, name = "PullLayout: Refresh And LoadMore") public class QDPullRefreshAndLoadMoreTestFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if(pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP){ onRefreshData(); }else if(pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM){ onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData(){ List data = new ArrayList<>(); long id = System.currentTimeMillis(); for(int i = 0; i < 10; i++){ data.add("onRefreshData-" + id + "-"+ i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore(){ List data = new ArrayList<>(); long id = System.currentTimeMillis(); for(int i = 0; i < 10; i++){ data.add("onLoadMore-" + id + "-"+ i); } mAdapter.append(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/pullLayout/QDPullVerticalTestFragment.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.fragment.components.pullLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @LatestVisitRecord @Widget(group = Group.Other, name = "PullLayout: Vertical Test") public class QDPullVerticalTestFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_vertical_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { mPullLayout.finishActionRun(pullAction); } }, 1000); } }); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceFragment.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.fragment.components.qqface; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2016-12-22 */ @Widget(group = Group.Lab, widgetClass = QMUIQQFaceView.class, iconRes = R.mipmap.icon_grid_qq_face_view) public class QDQQFaceFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDQQFaceUsageFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDQQFaceUsageFragment fragment = new QDQQFaceUsageFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDQQFacePerformanceTestFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDQQFacePerformanceTestFragment fragment = new QDQQFacePerformanceTestFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFacePerformanceTestFragment.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.fragment.components.qqface; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentFixModeFragment; import com.qmuiteam.qmuidemo.fragment.components.qqface.pageView.QDEmojiconPagerView; import com.qmuiteam.qmuidemo.fragment.components.qqface.pageView.QDQQFacePagerView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.HashMap; import java.util.Map; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-06-08 */ @Widget(group = Group.Other, name = "性能观测[微笑]") public class QDQQFacePerformanceTestFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabSegment) QMUITabSegment mTabSegment; @BindView(R.id.contentViewPager) ViewPager mContentViewPager; private Map mPageMap = new HashMap<>(); private Page mDestPage = Page.QMUIQQFaceView; private PagerAdapter mPagerAdapter = new PagerAdapter() { @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public int getCount() { return QDTabSegmentFixModeFragment.ContentPage.SIZE; } @Override public Object instantiateItem(final ViewGroup container, int position) { Page page = Page.getPage(position); View view = getPageView(page); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.addView(view, params); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public CharSequence getPageTitle(int position) { Page page = Page.getPage(position); if (page == Page.QMUIQQFaceView) { return "QMUI实现方案性能"; } else { return "ImageSpan实现方案性能"; } } }; @Override protected View onCreateView() { View rootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_tab_viewpager_layout, null); ButterKnife.bind(this, rootView); initTopBar(); initTabAndPager(); return rootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); } private void initTabAndPager() { mContentViewPager.setAdapter(mPagerAdapter); mContentViewPager.setCurrentItem(mDestPage.getPosition(), false); mTabSegment.setupWithViewPager(mContentViewPager, true); mTabSegment.addOnTabSelectedListener(new QMUITabSegment.OnTabSelectedListener() { @Override public void onTabSelected(int index) { } @Override public void onTabUnselected(int index) { } @Override public void onTabReselected(int index) { } @Override public void onDoubleTap(int index) { } }); } private View getPageView(Page page) { View view = mPageMap.get(page); if (view == null) { if (page == Page.QMUIQQFaceView) { view = new QDQQFacePagerView(getContext()); } else if (page == Page.EmojiconTextView) { view = new QDEmojiconPagerView(getContext()); } mPageMap.put(page, view); } return view; } public enum Page { QMUIQQFaceView(0), EmojiconTextView(1); private final int position; Page(int pos) { position = pos; } public static Page getPage(int position) { switch (position) { case 0: return QMUIQQFaceView; case 1: return EmojiconTextView; default: return QMUIQQFaceView; } } public int getPosition() { return position; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceTestData.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.fragment.components.qqface; import android.graphics.Color; import android.text.SpannableString; import android.text.Spanned; import android.view.View; import android.widget.Toast; import com.qmuiteam.qmui.span.QMUITouchableSpan; import java.util.ArrayList; /** * @author cginechen * @date 2016-12-22 */ public class QDQQFaceTestData { private ArrayList mList = new ArrayList<>(); public QDQQFaceTestData() { for (int i = 0; i < 100; i++) { String topic = "#表情[发呆][微笑]大战"; String at = "@伟大的[发呆]工程师"; String url = "https://qmuiteam.com?abcdefchigklmnopqrst"; String text = "index = " + i + " : " + at + ",人生就是要不断[微笑]\n[微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑]" + url + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[得意][微笑][撇嘴][色][微笑][得意][流泪][害羞][闭嘴][睡][微笑][微笑][微笑]" + "[微笑][微笑][惊讶][微笑][微笑][微笑][微笑][发怒][微笑]\n[微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][调皮][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][呲牙][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "人生就是要不断[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]\n\n[微笑][微笑][微笑]" + "[微笑][微笑]也会出现不合格的[这是不合格的标签]其它表情[发呆][发呆][发呆][发呆][发呆] " + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][发呆][发呆][发呆][发呆] " + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + topic + "[微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑]\n[微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][发呆][发呆][发呆][发呆][发呆][微笑][微笑][微笑]"; SpannableString sb = new SpannableString(text); sb.setSpan(new QMUITouchableSpan(Color.BLUE, Color.BLACK, Color.GRAY, Color.GREEN) { @Override public void onSpanClick(View widget) { Toast.makeText(widget.getContext(), "点击了@", Toast.LENGTH_SHORT).show(); } }, text.indexOf(at), text.indexOf(at) + at.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); sb.setSpan(new QMUITouchableSpan(Color.RED, Color.BLACK, Color.YELLOW, Color.GREEN) { @Override public void onSpanClick(View widget) { Toast.makeText(widget.getContext(), "点击了url", Toast.LENGTH_SHORT).show(); } }, text.indexOf(url), text.indexOf(url) + url.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); sb.setSpan(new QMUITouchableSpan(Color.RED, Color.BLACK, Color.YELLOW, Color.GREEN) { @Override public void onSpanClick(View widget) { Toast.makeText(widget.getContext(), "点击了话题", Toast.LENGTH_SHORT).show(); } }, text.indexOf(topic), text.indexOf(topic) + topic.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); mList.add(sb); } } public ArrayList getList() { return mList; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.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.qmuidemo.fragment.components.qqface import android.content.Context import android.graphics.Color import android.graphics.Paint import android.text.SpannableString import android.text.Spanned import android.text.TextUtils import android.text.style.LineHeightSpan import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.widget.TextView import android.widget.Toast import androidx.core.content.ContextCompat import butterknife.BindView import butterknife.ButterKnife import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmui.kotlin.onClick import com.qmuiteam.qmui.qqface.QMUIQQFaceView import com.qmuiteam.qmui.span.QMUITouchableSpan import com.qmuiteam.qmui.type.SerialLineIndentHandler import com.qmuiteam.qmui.type.parser.EmojiTextParser import com.qmuiteam.qmui.type.parser.TextParser import com.qmuiteam.qmui.type.view.LineTypeView import com.qmuiteam.qmui.type.view.MarqueeTypeView import com.qmuiteam.qmui.util.QMUIColorHelper import com.qmuiteam.qmui.util.QMUIDisplayHelper import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmuidemo.QDQQFaceManager import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.BaseFragment import com.qmuiteam.qmuidemo.lib.Group import com.qmuiteam.qmuidemo.lib.annotation.Widget import com.qmuiteam.qmuidemo.manager.QDDataManager import java.util.regex.Pattern /** * @author cginechen * @date 2016-12-24 */ class B(val mHeight: Int): LineHeightSpan { override fun chooseHeight(text: CharSequence?, start: Int, end: Int, spanstartv: Int, lineHeight: Int, fm: Paint.FontMetricsInt) { // 参考官方 API 29 提供的 Standard 而进行修改 if (fm.descent <= fm.bottom && fm.ascent >= fm.top) { if (fm.descent > mHeight) { // Show as much descent as possible fm.descent = Math.min(mHeight, fm.descent) fm.bottom = fm.descent fm.ascent = 0 fm.top = fm.ascent } else if (-fm.ascent + fm.descent > mHeight) { // Show all descent, and as much ascent as possible fm.bottom = fm.descent fm.ascent = -mHeight + fm.descent fm.top = fm.ascent } else { // Show proportionally additional ascent / top & descent / bottom val additional: Int = mHeight - (-fm.top + fm.bottom) // Round up for the negative values and down for the positive values (arbitrary choice) // So that bottom - top equals additional even if it's an odd number. fm.top -= Math.ceil((additional / 2.0f).toDouble()).toInt() fm.bottom += Math.floor((additional / 2.0f).toDouble()).toInt() fm.ascent = fm.top fm.descent = fm.bottom } } else { val originHeight = fm.descent - fm.ascent // If original height is not positive, do nothing. if (originHeight <= 0) { return } if (originHeight < mHeight) { // Show proportionally additional ascent / top & descent / bottom val additional: Int = mHeight - originHeight // Round up for the negative values and down for the positive values (arbitrary choice) // So that bottom - top equals additional even if it's an odd number. fm.ascent -= Math.ceil((additional / 2.0f).toDouble()).toInt() fm.top = fm.ascent fm.descent += Math.floor((additional / 2.0f).toDouble()).toInt() fm.bottom = fm.descent } else { var ratio: Float = mHeight * 1.0f / originHeight fm.descent = Math.round(fm.descent * ratio) fm.ascent = fm.descent - mHeight ratio = mHeight * 1.0f / (fm.bottom - fm.top) fm.bottom = Math.round(fm.bottom * ratio) fm.top = fm.bottom - mHeight } } } } class Test(context: Context, attrs: AttributeSet): TextView(context, attrs){ init { setBackgroundColor(Color.RED) text = SpannableString("呵呵བོད་སྐད").apply { setSpan(B(80), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(80, MeasureSpec.EXACTLY)) } } @Widget(group = Group.Other, name = "QQ表情使用展示") @LatestVisitRecord class QDQQFaceUsageFragment : BaseFragment() { @JvmField @BindView(R.id.topbar) var mTopBar: QMUITopBarLayout? = null @JvmField @BindView(R.id.marquee1) var mMarqueeTypeView1: MarqueeTypeView? = null @JvmField @BindView(R.id.marquee2) var mMarqueeTypeView2: MarqueeTypeView? = null @JvmField @BindView(R.id.line_type_1) var mLineType1: LineTypeView? = null @JvmField @BindView(R.id.line_type_2) var mLineType2: LineTypeView? = null @JvmField @BindView(R.id.line_type_3) var mLineType3: LineTypeView? = null @JvmField @BindView(R.id.qqface1) var mQQFace1: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface2) var mQQFace2: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface3) var mQQFace3: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface4) var mQQFace4: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface5) var mQQFace5: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface6) var mQQFace6: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface7) var mQQFace7: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface8) var mQQFace8: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface9) var mQQFace9: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface10) var mQQFace10: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface11) var mQQFace11: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface12) var mQQFace12: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface13) var mQQFace13: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface14) var mQQFace14: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface15) var mQQFace15: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface16) var mQQFace16: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface17) var mQQFace17: QMUIQQFaceView? = null @JvmField @BindView(R.id.qqface18) var mQQFace18: QMUIQQFaceView? = null override fun onCreateView(): View { val view = LayoutInflater.from(context).inflate(R.layout.fragment_qqface_layout, null) ButterKnife.bind(this, view) initTopBar() initData() return view } private fun initTopBar() { mTopBar!!.addLeftBackImageButton().setOnClickListener { popBackStack() } mTopBar!!.setTitle(QDDataManager.getInstance().getName(this.javaClass)) } private fun initData() { val textParser: TextParser = EmojiTextParser(QDQQFaceManager.getInstance()) { true } mMarqueeTypeView1!!.fadeWidth = QMUIDisplayHelper.dp2px(context, 40).toFloat() mMarqueeTypeView1!!.textParser = textParser mMarqueeTypeView1!!.text = "🙃🙃🙃🙃飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示" mMarqueeTypeView2!!.fadeWidth = QMUIDisplayHelper.dp2px(context, 40).toFloat() mMarqueeTypeView2!!.textParser = textParser mMarqueeTypeView2!!.text = "[大哭]我太短了,实在是飘不动了" mLineType1!!.textParser = textParser val lineLayout = mLineType1!!.lineLayout lineLayout.maxLines = 6 lineLayout.ellipsize = TextUtils.TruncateAt.END lineLayout.moreText = "更多" lineLayout.moreUnderlineHeight = QMUIDisplayHelper.dp2px(context, 2) lineLayout.moreTextColor = Color.RED lineLayout.moreUnderlineColor = Color.BLUE mLineType1!!.lineHeight = QMUIDisplayHelper.dp2px(context, 36) mLineType1!!.textColor = Color.BLACK mLineType1!!.textSize = QMUIDisplayHelper.sp2px(context, 15).toFloat() mLineType1!!.text = "QMUI Android 的设计[微笑]目的🙃🙃🙃🙃是用于辅助快速搭建一个具备基本设计还原[微笑]效果的 Android 项目," + "同时利用自身[微笑]提供的丰富控件及兼容处理,让开[微笑]发者能专注于业务需求而无需耗费[微笑]精力在基础代[微笑]码的设计上。" + "不管是新项目的创建,或是已有项[微笑]目的维护,均可使开[微笑]发效率和项目[微笑]质量得到大幅度提升。" mLineType1!!.addBgEffect(10, 16, QMUIColorHelper.setColorAlpha(Color.RED, 0.5f)) mLineType1!!.addClickEffect(20, 30, { isPressed -> if (isPressed) Color.RED else Color.BLUE }, { isPressed -> if (isPressed) Color.BLUE else Color.RED } ) { start, end -> Toast.makeText(context, "你点${start}-${end}干嘛", Toast.LENGTH_SHORT).show() } mLineType1!!.addClickEffect(44, 82, { isPressed -> if (isPressed) Color.RED else Color.BLUE }, { isPressed -> if (isPressed) Color.BLUE else Color.RED } ) { start, end -> Toast.makeText(context, "你点${start}-${end}干嘛", Toast.LENGTH_SHORT).show() } mLineType1!!.onClick { Toast.makeText(context, "你点整个 LineTypeView 干嘛", Toast.LENGTH_SHORT).show() } mLineType2!!.textParser = textParser mLineType2!!.lineHeight = QMUIDisplayHelper.dp2px(context, 36) mLineType2!!.textColor = Color.BLACK mLineType2!!.textSize = QMUIDisplayHelper.sp2px(context, 15).toFloat() val content2 = "a.这一条很重要,你要仔细研读研读。\n" + "b.这一条不重要,但是有很多很多很多很多很多很多很多很多内容。。\n" + "c.这一条特别重要,但是我也不知道对不对,只能放这里了,哈哈哈哈。\n" mLineType2!!.text = content2 val pairs = arrayListOf>() val pattern = Pattern.compile("([a-z]+\\.)") val matcher = pattern.matcher(content2) while (matcher.find()){ pairs.add(matcher.start() to matcher.end() - 1) } pairs.forEach { mLineType2!!.addTextColorEffect(it.first, it.second, Color.LTGRAY) } mLineType2!!.lineLayout.lineIndentHandler = SerialLineIndentHandler(pairs) mLineType3!!.textParser = textParser mLineType3!!.lineHeight = QMUIDisplayHelper.dp2px(context, 36) mLineType3!!.textColor = Color.BLACK mLineType3!!.textSize = QMUIDisplayHelper.sp2px(context, 15).toFloat() mLineType3!!.text = "འདི་བཞིན་གྱི་ཡིད་བརྙན་གྱི་ཚོགས་མང་པོ་ཞིག་གིས་ཞེ་དྲག་བསམ་གཞིག་གི་བར་སྟོང་ཡངས་པོར་ཕྱེས་འགྲོ" mQQFace1!!.text = "这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示" mQQFace2!!.text = "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。" mQQFace3!!.text = "这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示" mQQFace4!!.text = "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。" mQQFace5!!.text = "这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示" mQQFace6!!.text = "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行。" mQQFace7!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" mQQFace8!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" mQQFace9!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" mQQFace10!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑]" mQQFace11!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑]" mQQFace12!!.text = "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑]" mQQFace13!!.text = "表情可以和字体一起变大[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑][微笑]" + "[微笑][微笑][微笑][微笑][微笑]" val topic = "#[发呆][微笑]话题" val text = "这是一段文本,为了测量 span 的点击在不同 Gravity 下能否正常工作。$topic" val sb = SpannableString(text) val span: QMUITouchableSpan = object : QMUITouchableSpan( mQQFace14, R.attr.app_skin_span_normal_text_color, R.attr.app_skin_span_pressed_text_color, R.attr.app_skin_span_normal_bg_color, R.attr.app_skin_span_pressed_bg_color ) { override fun onSpanClick(widget: View) { Toast.makeText(widget.context, "点击了话题", Toast.LENGTH_SHORT).show() } } span.setIsNeedUnderline(true) sb.setSpan(span, text.indexOf(topic), text.indexOf(topic) + topic.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) mQQFace14!!.text = sb mQQFace15!!.text = sb mQQFace15!!.setLinkUnderLineColor(Color.RED) mQQFace16!!.text = sb mQQFace16!!.setLinkUnderLineHeight(QMUIDisplayHelper.dp2px(context, 4)) mQQFace16!!.setLinkUnderLineColor(ContextCompat.getColorStateList(requireContext(), R.color.s_app_color_blue_to_red)) mQQFace15!!.gravity = Gravity.CENTER mQQFace16!!.gravity = Gravity.RIGHT mQQFace17!!.setLinkUnderLineColor(Color.RED) mQQFace17!!.setNeedUnderlineForMoreText(true) mQQFace17!!.text = "这是一段文本,为了测量更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多" + "更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多" + "更多更多更多更多更多更多更多更多更多更多更多更多更多更多更多的显示情况" mQQFace18!!.setParagraphSpace(QMUIDisplayHelper.dp2px(context, 20)) mQQFace18!!.text = """ 这是一段文本,为[微笑]了测量多段落[微笑] 这是一段文本,为[微笑]了测量多段落[微笑] 这是一段文本,为[微笑]了测量多段落[微笑] """.trimIndent() } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/EmojiCache.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.fragment.components.qqface.emojicon; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.core.content.ContextCompat; import androidx.collection.LruCache; public class EmojiCache { //caceh里面默认只存放32个表情 private static final int EMOJI_CACHE_SIZE = 32; private static EmojiCache _instance; public static void createInstance(int cacheSize) { if(_instance == null) { _instance = new EmojiCache(cacheSize); } } public static EmojiCache getInstance() { if (_instance == null) { createInstance(EMOJI_CACHE_SIZE); } return _instance; } private LruCache mCache; public EmojiCache(int cacheSize) { mCache = new LruCache(cacheSize) { @Override protected int sizeOf(Integer key, Drawable value) { return 1; } @Override protected void entryRemoved(boolean evicted, Integer key, Drawable oldValue, Drawable newValue) { //这种情况,可能该drawable还在页面使用中,不能随便recycle。这里解除引用即可,gc会自动清除 // if (oldValue instanceof BitmapDrawable) { // ((BitmapDrawable)oldValue).getBitmap().recycle(); // } } }; } public Drawable getDrawable(Context context, int resourceId) { Drawable drawable = mCache.get(resourceId); if (drawable == null) { drawable = ContextCompat.getDrawable(context, resourceId); mCache.put(resourceId, drawable); } return drawable; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/EmojiconHandler.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.fragment.components.qqface.emojicon; import android.content.Context; import androidx.collection.ArrayMap; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.util.Log; import android.util.SparseIntArray; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.R; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * 用于作性能比较的控件。 */ @SuppressWarnings({"JavaDoc", "UnusedReturnValue", "unused", "WeakerAccess"}) public final class EmojiconHandler { private static final HashMap sQQFaceMap = new HashMap<>(); private static final List mQQFaceList = new ArrayList<>(); private static final SparseIntArray sEmojisMap = new SparseIntArray(846); private static final SparseIntArray sSoftbanksMap = new SparseIntArray(471); private static final ArrayMap mQQFaceFileNameList = new ArrayMap<>();//存储QQ表情对应的文件名,方便混淆后可以获取到原文件名 /** * 表情的放大倍数 */ private static final float EMOJIICON_SCALE = 1.2f; /** * 表情的偏移值 */ private static final int EMOJIICON_TRANSLATE_Y = 0; private static final int QQFACE_TRANSLATE_Y = QMUIDisplayHelper.dpToPx(1); static { long start = System.currentTimeMillis(); mQQFaceList.add(new QQFace("[微笑]", R.drawable.smiley_0)); mQQFaceList.add(new QQFace("[撇嘴]", R.drawable.smiley_1)); mQQFaceList.add(new QQFace("[色]", R.drawable.smiley_2)); mQQFaceList.add(new QQFace("[发呆]", R.drawable.smiley_3)); mQQFaceList.add(new QQFace("[得意]", R.drawable.smiley_4)); mQQFaceList.add(new QQFace("[流泪]", R.drawable.smiley_5)); mQQFaceList.add(new QQFace("[害羞]", R.drawable.smiley_6)); mQQFaceList.add(new QQFace("[闭嘴]", R.drawable.smiley_7)); mQQFaceList.add(new QQFace("[睡]", R.drawable.smiley_8)); mQQFaceList.add(new QQFace("[大哭]", R.drawable.smiley_9)); mQQFaceList.add(new QQFace("[尴尬]", R.drawable.smiley_10)); mQQFaceList.add(new QQFace("[发怒]", R.drawable.smiley_11)); mQQFaceList.add(new QQFace("[调皮]", R.drawable.smiley_12)); mQQFaceList.add(new QQFace("[呲牙]", R.drawable.smiley_13)); mQQFaceList.add(new QQFace("[惊讶]", R.drawable.smiley_14)); mQQFaceList.add(new QQFace("[难过]", R.drawable.smiley_15)); mQQFaceList.add(new QQFace("[酷]", R.drawable.smiley_16)); mQQFaceList.add(new QQFace("[冷汗]", R.drawable.smiley_17)); mQQFaceList.add(new QQFace("[抓狂]", R.drawable.smiley_18)); mQQFaceList.add(new QQFace("[吐]", R.drawable.smiley_19)); mQQFaceList.add(new QQFace("[偷笑]", R.drawable.smiley_20)); mQQFaceList.add(new QQFace("[可爱]", R.drawable.smiley_21)); mQQFaceList.add(new QQFace("[白眼]", R.drawable.smiley_22)); mQQFaceList.add(new QQFace("[傲慢]", R.drawable.smiley_23)); mQQFaceList.add(new QQFace("[饥饿]", R.drawable.smiley_24)); mQQFaceList.add(new QQFace("[困]", R.drawable.smiley_25)); mQQFaceList.add(new QQFace("[惊恐]", R.drawable.smiley_26)); mQQFaceList.add(new QQFace("[流汗]", R.drawable.smiley_27)); mQQFaceList.add(new QQFace("[憨笑]", R.drawable.smiley_28)); mQQFaceList.add(new QQFace("[大兵]", R.drawable.smiley_29)); mQQFaceList.add(new QQFace("[奋斗]", R.drawable.smiley_30)); mQQFaceList.add(new QQFace("[咒骂]", R.drawable.smiley_31)); mQQFaceList.add(new QQFace("[疑问]", R.drawable.smiley_32)); mQQFaceList.add(new QQFace("[嘘]", R.drawable.smiley_33)); mQQFaceList.add(new QQFace("[晕]", R.drawable.smiley_34)); mQQFaceList.add(new QQFace("[折磨]", R.drawable.smiley_35)); mQQFaceList.add(new QQFace("[衰]", R.drawable.smiley_36)); mQQFaceList.add(new QQFace("[骷髅]", R.drawable.smiley_37)); mQQFaceList.add(new QQFace("[敲打]", R.drawable.smiley_38)); mQQFaceList.add(new QQFace("[再见]", R.drawable.smiley_39)); mQQFaceList.add(new QQFace("[擦汗]", R.drawable.smiley_40)); mQQFaceList.add(new QQFace("[抠鼻]", R.drawable.smiley_41)); mQQFaceList.add(new QQFace("[鼓掌]", R.drawable.smiley_42)); mQQFaceList.add(new QQFace("[糗大了]", R.drawable.smiley_43)); mQQFaceList.add(new QQFace("[坏笑]", R.drawable.smiley_44)); mQQFaceList.add(new QQFace("[左哼哼]", R.drawable.smiley_45)); mQQFaceList.add(new QQFace("[右哼哼]", R.drawable.smiley_46)); mQQFaceList.add(new QQFace("[哈欠]", R.drawable.smiley_47)); mQQFaceList.add(new QQFace("[鄙视]", R.drawable.smiley_48)); mQQFaceList.add(new QQFace("[委屈]", R.drawable.smiley_49)); mQQFaceList.add(new QQFace("[快哭了]", R.drawable.smiley_50)); mQQFaceList.add(new QQFace("[阴险]", R.drawable.smiley_51)); mQQFaceList.add(new QQFace("[亲亲]", R.drawable.smiley_52)); mQQFaceList.add(new QQFace("[吓]", R.drawable.smiley_53)); mQQFaceList.add(new QQFace("[可怜]", R.drawable.smiley_54)); mQQFaceList.add(new QQFace("[菜刀]", R.drawable.smiley_55)); mQQFaceList.add(new QQFace("[西瓜]", R.drawable.smiley_56)); mQQFaceList.add(new QQFace("[啤酒]", R.drawable.smiley_57)); mQQFaceList.add(new QQFace("[篮球]", R.drawable.smiley_58)); mQQFaceList.add(new QQFace("[乒乓]", R.drawable.smiley_59)); mQQFaceList.add(new QQFace("[咖啡]", R.drawable.smiley_60)); mQQFaceList.add(new QQFace("[饭]", R.drawable.smiley_61)); mQQFaceList.add(new QQFace("[猪头]", R.drawable.smiley_62)); mQQFaceList.add(new QQFace("[玫瑰]", R.drawable.smiley_63)); mQQFaceList.add(new QQFace("[凋谢]", R.drawable.smiley_64)); mQQFaceList.add(new QQFace("[示爱]", R.drawable.smiley_65)); mQQFaceList.add(new QQFace("[爱心]", R.drawable.smiley_66)); mQQFaceList.add(new QQFace("[心碎]", R.drawable.smiley_67)); mQQFaceList.add(new QQFace("[蛋糕]", R.drawable.smiley_68)); mQQFaceList.add(new QQFace("[闪电]", R.drawable.smiley_69)); mQQFaceList.add(new QQFace("[炸弹]", R.drawable.smiley_70)); mQQFaceList.add(new QQFace("[刀]", R.drawable.smiley_71)); mQQFaceList.add(new QQFace("[足球]", R.drawable.smiley_72)); mQQFaceList.add(new QQFace("[瓢虫]", R.drawable.smiley_73)); mQQFaceList.add(new QQFace("[便便]", R.drawable.smiley_74)); mQQFaceList.add(new QQFace("[月亮]", R.drawable.smiley_75)); mQQFaceList.add(new QQFace("[太阳]", R.drawable.smiley_76)); mQQFaceList.add(new QQFace("[礼物]", R.drawable.smiley_77)); mQQFaceList.add(new QQFace("[拥抱]", R.drawable.smiley_78)); mQQFaceList.add(new QQFace("[强]", R.drawable.smiley_79)); mQQFaceList.add(new QQFace("[弱]", R.drawable.smiley_80)); mQQFaceList.add(new QQFace("[握手]", R.drawable.smiley_81)); mQQFaceList.add(new QQFace("[胜利]", R.drawable.smiley_82)); mQQFaceList.add(new QQFace("[抱拳]", R.drawable.smiley_83)); mQQFaceList.add(new QQFace("[勾引]", R.drawable.smiley_84)); mQQFaceList.add(new QQFace("[拳头]", R.drawable.smiley_85)); mQQFaceList.add(new QQFace("[差劲]", R.drawable.smiley_86)); mQQFaceList.add(new QQFace("[爱你]", R.drawable.smiley_87)); mQQFaceList.add(new QQFace("[NO]", R.drawable.smiley_88)); mQQFaceList.add(new QQFace("[OK]", R.drawable.smiley_89)); mQQFaceList.add(new QQFace("[爱情]", R.drawable.smiley_90)); mQQFaceList.add(new QQFace("[飞吻]", R.drawable.smiley_91)); mQQFaceList.add(new QQFace("[跳跳]", R.drawable.smiley_92)); mQQFaceList.add(new QQFace("[发抖]", R.drawable.smiley_93)); mQQFaceList.add(new QQFace("[怄火]", R.drawable.smiley_94)); mQQFaceList.add(new QQFace("[转圈]", R.drawable.smiley_95)); mQQFaceList.add(new QQFace("[磕头]", R.drawable.smiley_96)); mQQFaceList.add(new QQFace("[回头]", R.drawable.smiley_97)); mQQFaceList.add(new QQFace("[跳绳]", R.drawable.smiley_98)); mQQFaceList.add(new QQFace("[挥手]", R.drawable.smiley_99)); mQQFaceList.add(new QQFace("[激动]", R.drawable.smiley_100)); mQQFaceList.add(new QQFace("[街舞]", R.drawable.smiley_101)); mQQFaceList.add(new QQFace("[献吻]", R.drawable.smiley_102)); mQQFaceList.add(new QQFace("[左太极]", R.drawable.smiley_103)); mQQFaceList.add(new QQFace("[右太极]", R.drawable.smiley_104)); for (QQFace face : mQQFaceList) { sQQFaceMap.put(face.name, face.res); } mQQFaceFileNameList.put("[微笑]", "smiley_0"); mQQFaceFileNameList.put("[撇嘴]", "smiley_1"); mQQFaceFileNameList.put("[色]", "smiley_2"); mQQFaceFileNameList.put("[发呆]", "smiley_3"); mQQFaceFileNameList.put("[得意]", "smiley_4"); mQQFaceFileNameList.put("[流泪]", "smiley_5"); mQQFaceFileNameList.put("[害羞]", "smiley_6"); mQQFaceFileNameList.put("[闭嘴]", "smiley_7"); mQQFaceFileNameList.put("[睡]", "smiley_8"); mQQFaceFileNameList.put("[大哭]", "smiley_9"); mQQFaceFileNameList.put("[尴尬]", "smiley_10"); mQQFaceFileNameList.put("[发怒]", "smiley_11"); mQQFaceFileNameList.put("[调皮]", "smiley_12"); mQQFaceFileNameList.put("[呲牙]", "smiley_13"); mQQFaceFileNameList.put("[惊讶]", "smiley_14"); mQQFaceFileNameList.put("[难过]", "smiley_15"); mQQFaceFileNameList.put("[酷]", "smiley_16"); mQQFaceFileNameList.put("[冷汗]", "smiley_17"); mQQFaceFileNameList.put("[抓狂]", "smiley_18"); mQQFaceFileNameList.put("[吐]", "smiley_19"); mQQFaceFileNameList.put("[偷笑]", "smiley_20"); mQQFaceFileNameList.put("[可爱]", "smiley_21"); mQQFaceFileNameList.put("[白眼]", "smiley_22"); mQQFaceFileNameList.put("[傲慢]", "smiley_23"); mQQFaceFileNameList.put("[饥饿]", "smiley_24"); mQQFaceFileNameList.put("[困]", "smiley_25"); mQQFaceFileNameList.put("[惊恐]", "smiley_26"); mQQFaceFileNameList.put("[流汗]", "smiley_27"); mQQFaceFileNameList.put("[憨笑]", "smiley_28"); mQQFaceFileNameList.put("[大兵]", "smiley_29"); mQQFaceFileNameList.put("[奋斗]", "smiley_30"); mQQFaceFileNameList.put("[咒骂]", "smiley_31"); mQQFaceFileNameList.put("[疑问]", "smiley_32"); mQQFaceFileNameList.put("[嘘]", "smiley_33"); mQQFaceFileNameList.put("[晕]", "smiley_34"); mQQFaceFileNameList.put("[折磨]", "smiley_35"); mQQFaceFileNameList.put("[衰]", "smiley_36"); mQQFaceFileNameList.put("[骷髅]", "smiley_37"); mQQFaceFileNameList.put("[敲打]", "smiley_38"); mQQFaceFileNameList.put("[再见]", "smiley_39"); mQQFaceFileNameList.put("[擦汗]", "smiley_40"); mQQFaceFileNameList.put("[抠鼻]", "smiley_41"); mQQFaceFileNameList.put("[鼓掌]", "smiley_42"); mQQFaceFileNameList.put("[糗大了]", "smiley_43"); mQQFaceFileNameList.put("[坏笑]", "smiley_44"); mQQFaceFileNameList.put("[左哼哼]", "smiley_45"); mQQFaceFileNameList.put("[右哼哼]", "smiley_46"); mQQFaceFileNameList.put("[哈欠]", "smiley_47"); mQQFaceFileNameList.put("[鄙视]", "smiley_48"); mQQFaceFileNameList.put("[委屈]", "smiley_49"); mQQFaceFileNameList.put("[快哭了]", "smiley_50"); mQQFaceFileNameList.put("[阴险]", "smiley_51"); mQQFaceFileNameList.put("[亲亲]", "smiley_52"); mQQFaceFileNameList.put("[吓]", "smiley_53"); mQQFaceFileNameList.put("[可怜]", "smiley_54"); mQQFaceFileNameList.put("[菜刀]", "smiley_55"); mQQFaceFileNameList.put("[西瓜]", "smiley_56"); mQQFaceFileNameList.put("[啤酒]", "smiley_57"); mQQFaceFileNameList.put("[篮球]", "smiley_58"); mQQFaceFileNameList.put("[乒乓]", "smiley_59"); mQQFaceFileNameList.put("[咖啡]", "smiley_60"); mQQFaceFileNameList.put("[饭]", "smiley_61"); mQQFaceFileNameList.put("[猪头]", "smiley_62"); mQQFaceFileNameList.put("[玫瑰]", "smiley_63"); mQQFaceFileNameList.put("[凋谢]", "smiley_64"); mQQFaceFileNameList.put("[示爱]", "smiley_65"); mQQFaceFileNameList.put("[爱心]", "smiley_66"); mQQFaceFileNameList.put("[心碎]", "smiley_67"); mQQFaceFileNameList.put("[蛋糕]", "smiley_68"); mQQFaceFileNameList.put("[闪电]", "smiley_69"); mQQFaceFileNameList.put("[炸弹]", "smiley_70"); mQQFaceFileNameList.put("[刀]", "smiley_71"); mQQFaceFileNameList.put("[足球]", "smiley_72"); mQQFaceFileNameList.put("[瓢虫]", "smiley_73"); mQQFaceFileNameList.put("[便便]", "smiley_74"); mQQFaceFileNameList.put("[月亮]", "smiley_75"); mQQFaceFileNameList.put("[太阳]", "smiley_76"); mQQFaceFileNameList.put("[礼物]", "smiley_77"); mQQFaceFileNameList.put("[拥抱]", "smiley_78"); mQQFaceFileNameList.put("[强]", "smiley_79"); mQQFaceFileNameList.put("[弱]", "smiley_80"); mQQFaceFileNameList.put("[握手]", "smiley_81"); mQQFaceFileNameList.put("[胜利]", "smiley_82"); mQQFaceFileNameList.put("[抱拳]", "smiley_83"); mQQFaceFileNameList.put("[勾引]", "smiley_84"); mQQFaceFileNameList.put("[拳头]", "smiley_85"); mQQFaceFileNameList.put("[差劲]", "smiley_86"); mQQFaceFileNameList.put("[爱你]", "smiley_87"); mQQFaceFileNameList.put("[NO]", "smiley_88"); mQQFaceFileNameList.put("[OK]", "smiley_89"); mQQFaceFileNameList.put("[爱情]", "smiley_90"); mQQFaceFileNameList.put("[飞吻]", "smiley_91"); mQQFaceFileNameList.put("[跳跳]", "smiley_92"); mQQFaceFileNameList.put("[发抖]", "smiley_93"); mQQFaceFileNameList.put("[怄火]", "smiley_94"); mQQFaceFileNameList.put("[转圈]", "smiley_95"); mQQFaceFileNameList.put("[磕头]", "smiley_96"); mQQFaceFileNameList.put("[回头]", "smiley_97"); mQQFaceFileNameList.put("[跳绳]", "smiley_98"); mQQFaceFileNameList.put("[挥手]", "smiley_99"); mQQFaceFileNameList.put("[激动]", "smiley_100"); mQQFaceFileNameList.put("[街舞]", "smiley_101"); mQQFaceFileNameList.put("[献吻]", "smiley_102"); mQQFaceFileNameList.put("[左太极]", "smiley_103"); mQQFaceFileNameList.put("[右太极]", "smiley_104"); sEmojisMap.append(0x00a9, R.drawable.emoji_00a9); sEmojisMap.append(0x00ae, R.drawable.emoji_00ae); sEmojisMap.append(0x203c, R.drawable.emoji_203c); sEmojisMap.append(0x2049, R.drawable.emoji_2049); sEmojisMap.append(0x2122, R.drawable.emoji_2122); sEmojisMap.append(0x2139, R.drawable.emoji_2139); sEmojisMap.append(0x2194, R.drawable.emoji_2194); sEmojisMap.append(0x2195, R.drawable.emoji_2195); sEmojisMap.append(0x2196, R.drawable.emoji_2196); sEmojisMap.append(0x2197, R.drawable.emoji_2197); sEmojisMap.append(0x2198, R.drawable.emoji_2198); sEmojisMap.append(0x2199, R.drawable.emoji_2199); sEmojisMap.append(0x21a9, R.drawable.emoji_21a9); sEmojisMap.append(0x21aa, R.drawable.emoji_21aa); sEmojisMap.append(0x231a, R.drawable.emoji_231a); sEmojisMap.append(0x231b, R.drawable.emoji_231b); sEmojisMap.append(0x23e9, R.drawable.emoji_23e9); sEmojisMap.append(0x23ea, R.drawable.emoji_23ea); sEmojisMap.append(0x23eb, R.drawable.emoji_23eb); sEmojisMap.append(0x23ec, R.drawable.emoji_23ec); sEmojisMap.append(0x23f0, R.drawable.emoji_23f0); sEmojisMap.append(0x23f3, R.drawable.emoji_23f3); sEmojisMap.append(0x24c2, R.drawable.emoji_24c2); sEmojisMap.append(0x25aa, R.drawable.emoji_25aa); sEmojisMap.append(0x25ab, R.drawable.emoji_25ab); sEmojisMap.append(0x25b6, R.drawable.emoji_25b6); sEmojisMap.append(0x25c0, R.drawable.emoji_25c0); sEmojisMap.append(0x25fb, R.drawable.emoji_25fb); sEmojisMap.append(0x25fc, R.drawable.emoji_25fc); sEmojisMap.append(0x25fd, R.drawable.emoji_25fd); sEmojisMap.append(0x25fe, R.drawable.emoji_25fe); sEmojisMap.append(0x2600, R.drawable.emoji_2600); sEmojisMap.append(0x2601, R.drawable.emoji_2601); sEmojisMap.append(0x260e, R.drawable.emoji_260e); sEmojisMap.append(0x2611, R.drawable.emoji_2611); sEmojisMap.append(0x2614, R.drawable.emoji_2614); sEmojisMap.append(0x2615, R.drawable.emoji_2615); sEmojisMap.append(0x261d, R.drawable.emoji_261d); sEmojisMap.append(0x263a, R.drawable.emoji_263a); sEmojisMap.append(0x2648, R.drawable.emoji_2648); sEmojisMap.append(0x2649, R.drawable.emoji_2649); sEmojisMap.append(0x264a, R.drawable.emoji_264a); sEmojisMap.append(0x264b, R.drawable.emoji_264b); sEmojisMap.append(0x264c, R.drawable.emoji_264c); sEmojisMap.append(0x264d, R.drawable.emoji_264d); sEmojisMap.append(0x264e, R.drawable.emoji_264e); sEmojisMap.append(0x264f, R.drawable.emoji_264f); sEmojisMap.append(0x2650, R.drawable.emoji_2650); sEmojisMap.append(0x2651, R.drawable.emoji_2651); sEmojisMap.append(0x2652, R.drawable.emoji_2652); sEmojisMap.append(0x2653, R.drawable.emoji_2653); sEmojisMap.append(0x2660, R.drawable.emoji_2660); sEmojisMap.append(0x2663, R.drawable.emoji_2663); sEmojisMap.append(0x2665, R.drawable.emoji_2665); sEmojisMap.append(0x2666, R.drawable.emoji_2666); sEmojisMap.append(0x2668, R.drawable.emoji_2668); sEmojisMap.append(0x267b, R.drawable.emoji_267b); sEmojisMap.append(0x267f, R.drawable.emoji_267f); sEmojisMap.append(0x2693, R.drawable.emoji_2693); sEmojisMap.append(0x26a0, R.drawable.emoji_26a0); sEmojisMap.append(0x26a1, R.drawable.emoji_26a1); sEmojisMap.append(0x26aa, R.drawable.emoji_26aa); sEmojisMap.append(0x26ab, R.drawable.emoji_26ab); sEmojisMap.append(0x26bd, R.drawable.emoji_26bd); sEmojisMap.append(0x26be, R.drawable.emoji_26be); sEmojisMap.append(0x26c4, R.drawable.emoji_26c4); sEmojisMap.append(0x26c5, R.drawable.emoji_26c5); sEmojisMap.append(0x26ce, R.drawable.emoji_26ce); sEmojisMap.append(0x26d4, R.drawable.emoji_26d4); sEmojisMap.append(0x26ea, R.drawable.emoji_26ea); sEmojisMap.append(0x26f2, R.drawable.emoji_26f2); sEmojisMap.append(0x26f3, R.drawable.emoji_26f3); sEmojisMap.append(0x26f5, R.drawable.emoji_26f5); sEmojisMap.append(0x26fa, R.drawable.emoji_26fa); sEmojisMap.append(0x26fd, R.drawable.emoji_26fd); sEmojisMap.append(0x2702, R.drawable.emoji_2702); sEmojisMap.append(0x2705, R.drawable.emoji_2705); sEmojisMap.append(0x2708, R.drawable.emoji_2708); sEmojisMap.append(0x2709, R.drawable.emoji_2709); sEmojisMap.append(0x270a, R.drawable.emoji_270a); sEmojisMap.append(0x270b, R.drawable.emoji_270b); sEmojisMap.append(0x270c, R.drawable.emoji_270c); sEmojisMap.append(0x270f, R.drawable.emoji_270f); sEmojisMap.append(0x2712, R.drawable.emoji_2712); sEmojisMap.append(0x2714, R.drawable.emoji_2714); sEmojisMap.append(0x2716, R.drawable.emoji_2716); sEmojisMap.append(0x2728, R.drawable.emoji_2728); sEmojisMap.append(0x2733, R.drawable.emoji_2733); sEmojisMap.append(0x2734, R.drawable.emoji_2734); sEmojisMap.append(0x2744, R.drawable.emoji_2744); sEmojisMap.append(0x2747, R.drawable.emoji_2747); sEmojisMap.append(0x274c, R.drawable.emoji_274c); sEmojisMap.append(0x274e, R.drawable.emoji_274e); sEmojisMap.append(0x2753, R.drawable.emoji_2753); sEmojisMap.append(0x2754, R.drawable.emoji_2754); sEmojisMap.append(0x2755, R.drawable.emoji_2755); sEmojisMap.append(0x2757, R.drawable.emoji_2757); sEmojisMap.append(0x2764, R.drawable.emoji_2764); sEmojisMap.append(0x2795, R.drawable.emoji_2795); sEmojisMap.append(0x2796, R.drawable.emoji_2796); sEmojisMap.append(0x2797, R.drawable.emoji_2797); sEmojisMap.append(0x27a1, R.drawable.emoji_27a1); sEmojisMap.append(0x27b0, R.drawable.emoji_27b0); sEmojisMap.append(0x27bf, R.drawable.emoji_27bf); sEmojisMap.append(0x2934, R.drawable.emoji_2934); sEmojisMap.append(0x2935, R.drawable.emoji_2935); sEmojisMap.append(0x2b05, R.drawable.emoji_2b05); sEmojisMap.append(0x2b06, R.drawable.emoji_2b06); sEmojisMap.append(0x2b07, R.drawable.emoji_2b07); sEmojisMap.append(0x2b1b, R.drawable.emoji_2b1b); sEmojisMap.append(0x2b1c, R.drawable.emoji_2b1c); sEmojisMap.append(0x2b50, R.drawable.emoji_2b50); sEmojisMap.append(0x2b55, R.drawable.emoji_2b55); sEmojisMap.append(0x3030, R.drawable.emoji_3030); sEmojisMap.append(0x303d, R.drawable.emoji_303d); sEmojisMap.append(0x3297, R.drawable.emoji_3297); sEmojisMap.append(0x3299, R.drawable.emoji_3299); sEmojisMap.append(0x1f004, R.drawable.emoji_1f004); sEmojisMap.append(0x1f0cf, R.drawable.emoji_1f0cf); sEmojisMap.append(0x1f170, R.drawable.emoji_1f170); sEmojisMap.append(0x1f171, R.drawable.emoji_1f171); sEmojisMap.append(0x1f17e, R.drawable.emoji_1f17e); sEmojisMap.append(0x1f17f, R.drawable.emoji_1f17f); sEmojisMap.append(0x1f18e, R.drawable.emoji_1f18e); sEmojisMap.append(0x1f191, R.drawable.emoji_1f191); sEmojisMap.append(0x1f192, R.drawable.emoji_1f192); sEmojisMap.append(0x1f193, R.drawable.emoji_1f193); sEmojisMap.append(0x1f194, R.drawable.emoji_1f194); sEmojisMap.append(0x1f195, R.drawable.emoji_1f195); sEmojisMap.append(0x1f196, R.drawable.emoji_1f196); sEmojisMap.append(0x1f197, R.drawable.emoji_1f197); sEmojisMap.append(0x1f198, R.drawable.emoji_1f198); sEmojisMap.append(0x1f199, R.drawable.emoji_1f199); sEmojisMap.append(0x1f19a, R.drawable.emoji_1f19a); sEmojisMap.append(0x1f201, R.drawable.emoji_1f201); sEmojisMap.append(0x1f202, R.drawable.emoji_1f202); sEmojisMap.append(0x1f21a, R.drawable.emoji_1f21a); sEmojisMap.append(0x1f22f, R.drawable.emoji_1f22f); sEmojisMap.append(0x1f232, R.drawable.emoji_1f232); sEmojisMap.append(0x1f233, R.drawable.emoji_1f233); sEmojisMap.append(0x1f234, R.drawable.emoji_1f234); sEmojisMap.append(0x1f235, R.drawable.emoji_1f235); sEmojisMap.append(0x1f236, R.drawable.emoji_1f236); sEmojisMap.append(0x1f237, R.drawable.emoji_1f237); sEmojisMap.append(0x1f238, R.drawable.emoji_1f238); sEmojisMap.append(0x1f239, R.drawable.emoji_1f239); sEmojisMap.append(0x1f23a, R.drawable.emoji_1f23a); sEmojisMap.append(0x1f250, R.drawable.emoji_1f250); sEmojisMap.append(0x1f251, R.drawable.emoji_1f251); sEmojisMap.append(0x1f300, R.drawable.emoji_1f300); sEmojisMap.append(0x1f301, R.drawable.emoji_1f301); sEmojisMap.append(0x1f302, R.drawable.emoji_1f302); sEmojisMap.append(0x1f303, R.drawable.emoji_1f303); sEmojisMap.append(0x1f304, R.drawable.emoji_1f304); sEmojisMap.append(0x1f305, R.drawable.emoji_1f305); sEmojisMap.append(0x1f306, R.drawable.emoji_1f306); sEmojisMap.append(0x1f307, R.drawable.emoji_1f307); sEmojisMap.append(0x1f308, R.drawable.emoji_1f308); sEmojisMap.append(0x1f309, R.drawable.emoji_1f309); sEmojisMap.append(0x1f30a, R.drawable.emoji_1f30a); sEmojisMap.append(0x1f30b, R.drawable.emoji_1f30b); sEmojisMap.append(0x1f30c, R.drawable.emoji_1f30c); sEmojisMap.append(0x1f30d, R.drawable.emoji_1f30d); sEmojisMap.append(0x1f30e, R.drawable.emoji_1f30e); sEmojisMap.append(0x1f30f, R.drawable.emoji_1f30f); sEmojisMap.append(0x1f310, R.drawable.emoji_1f310); sEmojisMap.append(0x1f311, R.drawable.emoji_1f311); sEmojisMap.append(0x1f312, R.drawable.emoji_1f312); sEmojisMap.append(0x1f313, R.drawable.emoji_1f313); sEmojisMap.append(0x1f314, R.drawable.emoji_1f314); sEmojisMap.append(0x1f315, R.drawable.emoji_1f315); sEmojisMap.append(0x1f316, R.drawable.emoji_1f316); sEmojisMap.append(0x1f317, R.drawable.emoji_1f317); sEmojisMap.append(0x1f318, R.drawable.emoji_1f318); sEmojisMap.append(0x1f319, R.drawable.emoji_1f319); sEmojisMap.append(0x1f31a, R.drawable.emoji_1f31a); sEmojisMap.append(0x1f31b, R.drawable.emoji_1f31b); sEmojisMap.append(0x1f31c, R.drawable.emoji_1f31c); sEmojisMap.append(0x1f31d, R.drawable.emoji_1f31d); sEmojisMap.append(0x1f31e, R.drawable.emoji_1f31e); sEmojisMap.append(0x1f31f, R.drawable.emoji_1f31f); sEmojisMap.append(0x1f320, R.drawable.emoji_1f303); sEmojisMap.append(0x1f330, R.drawable.emoji_1f330); sEmojisMap.append(0x1f331, R.drawable.emoji_1f331); sEmojisMap.append(0x1f332, R.drawable.emoji_1f332); sEmojisMap.append(0x1f333, R.drawable.emoji_1f333); sEmojisMap.append(0x1f334, R.drawable.emoji_1f334); sEmojisMap.append(0x1f335, R.drawable.emoji_1f335); sEmojisMap.append(0x1f337, R.drawable.emoji_1f337); sEmojisMap.append(0x1f338, R.drawable.emoji_1f338); sEmojisMap.append(0x1f339, R.drawable.emoji_1f339); sEmojisMap.append(0x1f33a, R.drawable.emoji_1f33a); sEmojisMap.append(0x1f33b, R.drawable.emoji_1f33b); sEmojisMap.append(0x1f33c, R.drawable.emoji_1f33c); sEmojisMap.append(0x1f33d, R.drawable.emoji_1f33d); sEmojisMap.append(0x1f33e, R.drawable.emoji_1f33e); sEmojisMap.append(0x1f33f, R.drawable.emoji_1f33f); sEmojisMap.append(0x1f340, R.drawable.emoji_1f340); sEmojisMap.append(0x1f341, R.drawable.emoji_1f341); sEmojisMap.append(0x1f342, R.drawable.emoji_1f342); sEmojisMap.append(0x1f343, R.drawable.emoji_1f343); sEmojisMap.append(0x1f344, R.drawable.emoji_1f344); sEmojisMap.append(0x1f345, R.drawable.emoji_1f345); sEmojisMap.append(0x1f346, R.drawable.emoji_1f346); sEmojisMap.append(0x1f347, R.drawable.emoji_1f347); sEmojisMap.append(0x1f348, R.drawable.emoji_1f348); sEmojisMap.append(0x1f349, R.drawable.emoji_1f349); sEmojisMap.append(0x1f34a, R.drawable.emoji_1f34a); sEmojisMap.append(0x1f34b, R.drawable.emoji_1f34b); sEmojisMap.append(0x1f34c, R.drawable.emoji_1f34c); sEmojisMap.append(0x1f34d, R.drawable.emoji_1f34d); sEmojisMap.append(0x1f34e, R.drawable.emoji_1f34e); sEmojisMap.append(0x1f34f, R.drawable.emoji_1f34f); sEmojisMap.append(0x1f350, R.drawable.emoji_1f350); sEmojisMap.append(0x1f351, R.drawable.emoji_1f351); sEmojisMap.append(0x1f352, R.drawable.emoji_1f352); sEmojisMap.append(0x1f353, R.drawable.emoji_1f353); sEmojisMap.append(0x1f354, R.drawable.emoji_1f354); sEmojisMap.append(0x1f355, R.drawable.emoji_1f355); sEmojisMap.append(0x1f356, R.drawable.emoji_1f356); sEmojisMap.append(0x1f357, R.drawable.emoji_1f357); sEmojisMap.append(0x1f358, R.drawable.emoji_1f358); sEmojisMap.append(0x1f359, R.drawable.emoji_1f359); sEmojisMap.append(0x1f35a, R.drawable.emoji_1f35a); sEmojisMap.append(0x1f35b, R.drawable.emoji_1f35b); sEmojisMap.append(0x1f35c, R.drawable.emoji_1f35c); sEmojisMap.append(0x1f35d, R.drawable.emoji_1f35d); sEmojisMap.append(0x1f35e, R.drawable.emoji_1f35e); sEmojisMap.append(0x1f35f, R.drawable.emoji_1f35f); sEmojisMap.append(0x1f360, R.drawable.emoji_1f360); sEmojisMap.append(0x1f361, R.drawable.emoji_1f361); sEmojisMap.append(0x1f362, R.drawable.emoji_1f362); sEmojisMap.append(0x1f363, R.drawable.emoji_1f363); sEmojisMap.append(0x1f364, R.drawable.emoji_1f364); sEmojisMap.append(0x1f365, R.drawable.emoji_1f365); sEmojisMap.append(0x1f366, R.drawable.emoji_1f366); sEmojisMap.append(0x1f367, R.drawable.emoji_1f367); sEmojisMap.append(0x1f368, R.drawable.emoji_1f368); sEmojisMap.append(0x1f369, R.drawable.emoji_1f369); sEmojisMap.append(0x1f36a, R.drawable.emoji_1f36a); sEmojisMap.append(0x1f36b, R.drawable.emoji_1f36b); sEmojisMap.append(0x1f36c, R.drawable.emoji_1f36c); sEmojisMap.append(0x1f36d, R.drawable.emoji_1f36d); sEmojisMap.append(0x1f36e, R.drawable.emoji_1f36e); sEmojisMap.append(0x1f36f, R.drawable.emoji_1f36f); sEmojisMap.append(0x1f370, R.drawable.emoji_1f370); sEmojisMap.append(0x1f371, R.drawable.emoji_1f371); sEmojisMap.append(0x1f372, R.drawable.emoji_1f372); sEmojisMap.append(0x1f373, R.drawable.emoji_1f373); sEmojisMap.append(0x1f374, R.drawable.emoji_1f374); sEmojisMap.append(0x1f375, R.drawable.emoji_1f375); sEmojisMap.append(0x1f376, R.drawable.emoji_1f376); sEmojisMap.append(0x1f377, R.drawable.emoji_1f377); sEmojisMap.append(0x1f378, R.drawable.emoji_1f378); sEmojisMap.append(0x1f379, R.drawable.emoji_1f379); sEmojisMap.append(0x1f37a, R.drawable.emoji_1f37a); sEmojisMap.append(0x1f37b, R.drawable.emoji_1f37b); sEmojisMap.append(0x1f37c, R.drawable.emoji_1f37c); sEmojisMap.append(0x1f380, R.drawable.emoji_1f380); sEmojisMap.append(0x1f381, R.drawable.emoji_1f381); sEmojisMap.append(0x1f382, R.drawable.emoji_1f382); sEmojisMap.append(0x1f383, R.drawable.emoji_1f383); sEmojisMap.append(0x1f384, R.drawable.emoji_1f384); sEmojisMap.append(0x1f385, R.drawable.emoji_1f385); sEmojisMap.append(0x1f386, R.drawable.emoji_1f386); sEmojisMap.append(0x1f387, R.drawable.emoji_1f387); sEmojisMap.append(0x1f388, R.drawable.emoji_1f388); sEmojisMap.append(0x1f389, R.drawable.emoji_1f389); sEmojisMap.append(0x1f38a, R.drawable.emoji_1f38a); sEmojisMap.append(0x1f38b, R.drawable.emoji_1f38b); sEmojisMap.append(0x1f38c, R.drawable.emoji_1f38c); sEmojisMap.append(0x1f38d, R.drawable.emoji_1f38d); sEmojisMap.append(0x1f38e, R.drawable.emoji_1f38e); sEmojisMap.append(0x1f38f, R.drawable.emoji_1f38f); sEmojisMap.append(0x1f390, R.drawable.emoji_1f390); sEmojisMap.append(0x1f391, R.drawable.emoji_1f391); sEmojisMap.append(0x1f392, R.drawable.emoji_1f392); sEmojisMap.append(0x1f393, R.drawable.emoji_1f393); sEmojisMap.append(0x1f3a0, R.drawable.emoji_1f3a0); sEmojisMap.append(0x1f3a1, R.drawable.emoji_1f3a1); sEmojisMap.append(0x1f3a2, R.drawable.emoji_1f3a2); sEmojisMap.append(0x1f3a3, R.drawable.emoji_1f3a3); sEmojisMap.append(0x1f3a4, R.drawable.emoji_1f3a4); sEmojisMap.append(0x1f3a5, R.drawable.emoji_1f3a5); sEmojisMap.append(0x1f3a6, R.drawable.emoji_1f3a6); sEmojisMap.append(0x1f3a7, R.drawable.emoji_1f3a7); sEmojisMap.append(0x1f3a8, R.drawable.emoji_1f3a8); sEmojisMap.append(0x1f3a9, R.drawable.emoji_1f3a9); sEmojisMap.append(0x1f3aa, R.drawable.emoji_1f3aa); sEmojisMap.append(0x1f3ab, R.drawable.emoji_1f3ab); sEmojisMap.append(0x1f3ac, R.drawable.emoji_1f3ac); sEmojisMap.append(0x1f3ad, R.drawable.emoji_1f3ad); sEmojisMap.append(0x1f3ae, R.drawable.emoji_1f3ae); sEmojisMap.append(0x1f3af, R.drawable.emoji_1f3af); sEmojisMap.append(0x1f3b0, R.drawable.emoji_1f3b0); sEmojisMap.append(0x1f3b1, R.drawable.emoji_1f3b1); sEmojisMap.append(0x1f3b2, R.drawable.emoji_1f3b2); sEmojisMap.append(0x1f3b3, R.drawable.emoji_1f3b3); sEmojisMap.append(0x1f3b4, R.drawable.emoji_1f3b4); sEmojisMap.append(0x1f3b5, R.drawable.emoji_1f3b5); sEmojisMap.append(0x1f3b6, R.drawable.emoji_1f3b6); sEmojisMap.append(0x1f3b7, R.drawable.emoji_1f3b7); sEmojisMap.append(0x1f3b8, R.drawable.emoji_1f3b8); sEmojisMap.append(0x1f3b9, R.drawable.emoji_1f3b9); sEmojisMap.append(0x1f3ba, R.drawable.emoji_1f3ba); sEmojisMap.append(0x1f3bb, R.drawable.emoji_1f3bb); sEmojisMap.append(0x1f3bc, R.drawable.emoji_1f3bc); sEmojisMap.append(0x1f3bd, R.drawable.emoji_1f3bd); sEmojisMap.append(0x1f3be, R.drawable.emoji_1f3be); sEmojisMap.append(0x1f3bf, R.drawable.emoji_1f3bf); sEmojisMap.append(0x1f3c0, R.drawable.emoji_1f3c0); sEmojisMap.append(0x1f3c1, R.drawable.emoji_1f3c1); sEmojisMap.append(0x1f3c2, R.drawable.emoji_1f3c2); sEmojisMap.append(0x1f3c3, R.drawable.emoji_1f3c3); sEmojisMap.append(0x1f3c4, R.drawable.emoji_1f3c4); sEmojisMap.append(0x1f3c6, R.drawable.emoji_1f3c6); sEmojisMap.append(0x1f3c7, R.drawable.emoji_1f3c7); sEmojisMap.append(0x1f3c8, R.drawable.emoji_1f3c8); sEmojisMap.append(0x1f3c9, R.drawable.emoji_1f3c9); sEmojisMap.append(0x1f3ca, R.drawable.emoji_1f3ca); sEmojisMap.append(0x1f3e0, R.drawable.emoji_1f3e0); sEmojisMap.append(0x1f3e1, R.drawable.emoji_1f3e1); sEmojisMap.append(0x1f3e2, R.drawable.emoji_1f3e2); sEmojisMap.append(0x1f3e3, R.drawable.emoji_1f3e3); sEmojisMap.append(0x1f3e4, R.drawable.emoji_1f3e4); sEmojisMap.append(0x1f3e5, R.drawable.emoji_1f3e5); sEmojisMap.append(0x1f3e6, R.drawable.emoji_1f3e6); sEmojisMap.append(0x1f3e7, R.drawable.emoji_1f3e7); sEmojisMap.append(0x1f3e8, R.drawable.emoji_1f3e8); sEmojisMap.append(0x1f3e9, R.drawable.emoji_1f3e9); sEmojisMap.append(0x1f3ea, R.drawable.emoji_1f3ea); sEmojisMap.append(0x1f3eb, R.drawable.emoji_1f3eb); sEmojisMap.append(0x1f3ec, R.drawable.emoji_1f3ec); sEmojisMap.append(0x1f3ed, R.drawable.emoji_1f3ed); sEmojisMap.append(0x1f3ee, R.drawable.emoji_1f3ee); sEmojisMap.append(0x1f3ef, R.drawable.emoji_1f3ef); sEmojisMap.append(0x1f3f0, R.drawable.emoji_1f3f0); sEmojisMap.append(0x1f400, R.drawable.emoji_1f400); sEmojisMap.append(0x1f401, R.drawable.emoji_1f401); sEmojisMap.append(0x1f402, R.drawable.emoji_1f402); sEmojisMap.append(0x1f403, R.drawable.emoji_1f403); sEmojisMap.append(0x1f404, R.drawable.emoji_1f404); sEmojisMap.append(0x1f405, R.drawable.emoji_1f405); sEmojisMap.append(0x1f406, R.drawable.emoji_1f406); sEmojisMap.append(0x1f407, R.drawable.emoji_1f407); sEmojisMap.append(0x1f408, R.drawable.emoji_1f408); sEmojisMap.append(0x1f409, R.drawable.emoji_1f409); sEmojisMap.append(0x1f40a, R.drawable.emoji_1f40a); sEmojisMap.append(0x1f40b, R.drawable.emoji_1f40b); sEmojisMap.append(0x1f40c, R.drawable.emoji_1f40c); sEmojisMap.append(0x1f40d, R.drawable.emoji_1f40d); sEmojisMap.append(0x1f40e, R.drawable.emoji_1f40e); sEmojisMap.append(0x1f40f, R.drawable.emoji_1f40f); sEmojisMap.append(0x1f410, R.drawable.emoji_1f410); sEmojisMap.append(0x1f411, R.drawable.emoji_1f411); sEmojisMap.append(0x1f412, R.drawable.emoji_1f412); sEmojisMap.append(0x1f413, R.drawable.emoji_1f413); sEmojisMap.append(0x1f414, R.drawable.emoji_1f414); sEmojisMap.append(0x1f415, R.drawable.emoji_1f415); sEmojisMap.append(0x1f416, R.drawable.emoji_1f416); sEmojisMap.append(0x1f417, R.drawable.emoji_1f417); sEmojisMap.append(0x1f418, R.drawable.emoji_1f418); sEmojisMap.append(0x1f419, R.drawable.emoji_1f419); sEmojisMap.append(0x1f41a, R.drawable.emoji_1f41a); sEmojisMap.append(0x1f41b, R.drawable.emoji_1f41b); sEmojisMap.append(0x1f41c, R.drawable.emoji_1f41c); sEmojisMap.append(0x1f41d, R.drawable.emoji_1f41d); sEmojisMap.append(0x1f41e, R.drawable.emoji_1f41e); sEmojisMap.append(0x1f41f, R.drawable.emoji_1f41f); sEmojisMap.append(0x1f420, R.drawable.emoji_1f420); sEmojisMap.append(0x1f421, R.drawable.emoji_1f421); sEmojisMap.append(0x1f422, R.drawable.emoji_1f422); sEmojisMap.append(0x1f423, R.drawable.emoji_1f423); sEmojisMap.append(0x1f424, R.drawable.emoji_1f424); sEmojisMap.append(0x1f425, R.drawable.emoji_1f425); sEmojisMap.append(0x1f426, R.drawable.emoji_1f426); sEmojisMap.append(0x1f427, R.drawable.emoji_1f427); sEmojisMap.append(0x1f428, R.drawable.emoji_1f428); sEmojisMap.append(0x1f429, R.drawable.emoji_1f429); sEmojisMap.append(0x1f42a, R.drawable.emoji_1f42a); sEmojisMap.append(0x1f42b, R.drawable.emoji_1f42b); sEmojisMap.append(0x1f42c, R.drawable.emoji_1f42c); sEmojisMap.append(0x1f42d, R.drawable.emoji_1f42d); sEmojisMap.append(0x1f42e, R.drawable.emoji_1f42e); sEmojisMap.append(0x1f42f, R.drawable.emoji_1f42f); sEmojisMap.append(0x1f430, R.drawable.emoji_1f430); sEmojisMap.append(0x1f431, R.drawable.emoji_1f431); sEmojisMap.append(0x1f432, R.drawable.emoji_1f432); sEmojisMap.append(0x1f433, R.drawable.emoji_1f433); sEmojisMap.append(0x1f434, R.drawable.emoji_1f434); sEmojisMap.append(0x1f435, R.drawable.emoji_1f435); sEmojisMap.append(0x1f436, R.drawable.emoji_1f436); sEmojisMap.append(0x1f437, R.drawable.emoji_1f437); sEmojisMap.append(0x1f438, R.drawable.emoji_1f438); sEmojisMap.append(0x1f439, R.drawable.emoji_1f439); sEmojisMap.append(0x1f43a, R.drawable.emoji_1f43a); sEmojisMap.append(0x1f43b, R.drawable.emoji_1f43b); sEmojisMap.append(0x1f43c, R.drawable.emoji_1f43c); sEmojisMap.append(0x1f43d, R.drawable.emoji_1f43d); sEmojisMap.append(0x1f43e, R.drawable.emoji_1f43e); sEmojisMap.append(0x1f440, R.drawable.emoji_1f440); sEmojisMap.append(0x1f442, R.drawable.emoji_1f442); sEmojisMap.append(0x1f443, R.drawable.emoji_1f443); sEmojisMap.append(0x1f444, R.drawable.emoji_1f444); sEmojisMap.append(0x1f445, R.drawable.emoji_1f445); sEmojisMap.append(0x1f446, R.drawable.emoji_1f446); sEmojisMap.append(0x1f447, R.drawable.emoji_1f447); sEmojisMap.append(0x1f448, R.drawable.emoji_1f448); sEmojisMap.append(0x1f449, R.drawable.emoji_1f449); sEmojisMap.append(0x1f44a, R.drawable.emoji_1f44a); sEmojisMap.append(0x1f44b, R.drawable.emoji_1f44b); sEmojisMap.append(0x1f44c, R.drawable.emoji_1f44c); sEmojisMap.append(0x1f44d, R.drawable.emoji_1f44d); sEmojisMap.append(0x1f44e, R.drawable.emoji_1f44e); sEmojisMap.append(0x1f44f, R.drawable.emoji_1f44f); sEmojisMap.append(0x1f450, R.drawable.emoji_1f450); sEmojisMap.append(0x1f451, R.drawable.emoji_1f451); sEmojisMap.append(0x1f452, R.drawable.emoji_1f452); sEmojisMap.append(0x1f453, R.drawable.emoji_1f453); sEmojisMap.append(0x1f454, R.drawable.emoji_1f454); sEmojisMap.append(0x1f455, R.drawable.emoji_1f455); sEmojisMap.append(0x1f456, R.drawable.emoji_1f456); sEmojisMap.append(0x1f457, R.drawable.emoji_1f457); sEmojisMap.append(0x1f458, R.drawable.emoji_1f458); sEmojisMap.append(0x1f459, R.drawable.emoji_1f459); sEmojisMap.append(0x1f45a, R.drawable.emoji_1f45a); sEmojisMap.append(0x1f45b, R.drawable.emoji_1f45b); sEmojisMap.append(0x1f45c, R.drawable.emoji_1f45c); sEmojisMap.append(0x1f45d, R.drawable.emoji_1f45d); sEmojisMap.append(0x1f45e, R.drawable.emoji_1f45e); sEmojisMap.append(0x1f45f, R.drawable.emoji_1f45f); sEmojisMap.append(0x1f460, R.drawable.emoji_1f460); sEmojisMap.append(0x1f461, R.drawable.emoji_1f461); sEmojisMap.append(0x1f462, R.drawable.emoji_1f462); sEmojisMap.append(0x1f463, R.drawable.emoji_1f463); sEmojisMap.append(0x1f464, R.drawable.emoji_1f464); sEmojisMap.append(0x1f465, R.drawable.emoji_1f465); sEmojisMap.append(0x1f466, R.drawable.emoji_1f466); sEmojisMap.append(0x1f467, R.drawable.emoji_1f467); sEmojisMap.append(0x1f468, R.drawable.emoji_1f468); sEmojisMap.append(0x1f469, R.drawable.emoji_1f469); sEmojisMap.append(0x1f46a, R.drawable.emoji_1f46a); sEmojisMap.append(0x1f46b, R.drawable.emoji_1f46b); sEmojisMap.append(0x1f46c, R.drawable.emoji_1f46c); sEmojisMap.append(0x1f46d, R.drawable.emoji_1f46d); sEmojisMap.append(0x1f46e, R.drawable.emoji_1f46e); sEmojisMap.append(0x1f46f, R.drawable.emoji_1f46f); sEmojisMap.append(0x1f470, R.drawable.emoji_1f470); sEmojisMap.append(0x1f471, R.drawable.emoji_1f471); sEmojisMap.append(0x1f472, R.drawable.emoji_1f472); sEmojisMap.append(0x1f473, R.drawable.emoji_1f473); sEmojisMap.append(0x1f474, R.drawable.emoji_1f474); sEmojisMap.append(0x1f475, R.drawable.emoji_1f475); sEmojisMap.append(0x1f476, R.drawable.emoji_1f476); sEmojisMap.append(0x1f477, R.drawable.emoji_1f477); sEmojisMap.append(0x1f478, R.drawable.emoji_1f478); sEmojisMap.append(0x1f479, R.drawable.emoji_1f479); sEmojisMap.append(0x1f47a, R.drawable.emoji_1f47a); sEmojisMap.append(0x1f47b, R.drawable.emoji_1f47b); sEmojisMap.append(0x1f47c, R.drawable.emoji_1f47c); sEmojisMap.append(0x1f47d, R.drawable.emoji_1f47d); sEmojisMap.append(0x1f47e, R.drawable.emoji_1f47e); sEmojisMap.append(0x1f47f, R.drawable.emoji_1f47f); sEmojisMap.append(0x1f480, R.drawable.emoji_1f480); sEmojisMap.append(0x1f481, R.drawable.emoji_1f481); sEmojisMap.append(0x1f482, R.drawable.emoji_1f482); sEmojisMap.append(0x1f483, R.drawable.emoji_1f483); sEmojisMap.append(0x1f484, R.drawable.emoji_1f484); sEmojisMap.append(0x1f485, R.drawable.emoji_1f485); sEmojisMap.append(0x1f486, R.drawable.emoji_1f486); sEmojisMap.append(0x1f487, R.drawable.emoji_1f487); sEmojisMap.append(0x1f488, R.drawable.emoji_1f488); sEmojisMap.append(0x1f489, R.drawable.emoji_1f489); sEmojisMap.append(0x1f48a, R.drawable.emoji_1f48a); sEmojisMap.append(0x1f48b, R.drawable.emoji_1f48b); sEmojisMap.append(0x1f48c, R.drawable.emoji_1f48c); sEmojisMap.append(0x1f48d, R.drawable.emoji_1f48d); sEmojisMap.append(0x1f48e, R.drawable.emoji_1f48e); sEmojisMap.append(0x1f48f, R.drawable.emoji_1f48f); sEmojisMap.append(0x1f490, R.drawable.emoji_1f490); sEmojisMap.append(0x1f491, R.drawable.emoji_1f491); sEmojisMap.append(0x1f492, R.drawable.emoji_1f492); sEmojisMap.append(0x1f493, R.drawable.emoji_1f493); sEmojisMap.append(0x1f494, R.drawable.emoji_1f494); sEmojisMap.append(0x1f495, R.drawable.emoji_1f495); sEmojisMap.append(0x1f496, R.drawable.emoji_1f496); sEmojisMap.append(0x1f497, R.drawable.emoji_1f497); sEmojisMap.append(0x1f498, R.drawable.emoji_1f498); sEmojisMap.append(0x1f499, R.drawable.emoji_1f499); sEmojisMap.append(0x1f49a, R.drawable.emoji_1f49a); sEmojisMap.append(0x1f49b, R.drawable.emoji_1f49b); sEmojisMap.append(0x1f49c, R.drawable.emoji_1f49c); sEmojisMap.append(0x1f49d, R.drawable.emoji_1f49d); sEmojisMap.append(0x1f49e, R.drawable.emoji_1f49e); sEmojisMap.append(0x1f49f, R.drawable.emoji_1f49f); sEmojisMap.append(0x1f4a0, R.drawable.emoji_1f4a0); sEmojisMap.append(0x1f4a1, R.drawable.emoji_1f4a1); sEmojisMap.append(0x1f4a2, R.drawable.emoji_1f4a2); sEmojisMap.append(0x1f4a3, R.drawable.emoji_1f4a3); sEmojisMap.append(0x1f4a4, R.drawable.emoji_1f4a4); sEmojisMap.append(0x1f4a5, R.drawable.emoji_1f4a5); sEmojisMap.append(0x1f4a6, R.drawable.emoji_1f4a6); sEmojisMap.append(0x1f4a7, R.drawable.emoji_1f4a7); sEmojisMap.append(0x1f4a8, R.drawable.emoji_1f4a8); sEmojisMap.append(0x1f4a9, R.drawable.emoji_1f4a9); sEmojisMap.append(0x1f4aa, R.drawable.emoji_1f4aa); sEmojisMap.append(0x1f4ab, R.drawable.emoji_1f4ab); sEmojisMap.append(0x1f4ac, R.drawable.emoji_1f4ac); sEmojisMap.append(0x1f4ad, R.drawable.emoji_1f4ad); sEmojisMap.append(0x1f4ae, R.drawable.emoji_1f4ae); sEmojisMap.append(0x1f4af, R.drawable.emoji_1f4af); sEmojisMap.append(0x1f4b0, R.drawable.emoji_1f4b0); sEmojisMap.append(0x1f4b1, R.drawable.emoji_1f4b1); sEmojisMap.append(0x1f4b2, R.drawable.emoji_1f4b2); sEmojisMap.append(0x1f4b3, R.drawable.emoji_1f4b3); sEmojisMap.append(0x1f4b4, R.drawable.emoji_1f4b4); sEmojisMap.append(0x1f4b5, R.drawable.emoji_1f4b5); sEmojisMap.append(0x1f4b6, R.drawable.emoji_1f4b6); sEmojisMap.append(0x1f4b7, R.drawable.emoji_1f4b7); sEmojisMap.append(0x1f4b8, R.drawable.emoji_1f4b8); sEmojisMap.append(0x1f4b9, R.drawable.emoji_1f4b9); sEmojisMap.append(0x1f4ba, R.drawable.emoji_1f4ba); sEmojisMap.append(0x1f4bb, R.drawable.emoji_1f4bb); sEmojisMap.append(0x1f4bc, R.drawable.emoji_1f4bc); sEmojisMap.append(0x1f4bd, R.drawable.emoji_1f4bd); sEmojisMap.append(0x1f4be, R.drawable.emoji_1f4be); sEmojisMap.append(0x1f4bf, R.drawable.emoji_1f4bf); sEmojisMap.append(0x1f4c0, R.drawable.emoji_1f4c0); sEmojisMap.append(0x1f4c1, R.drawable.emoji_1f4c1); sEmojisMap.append(0x1f4c2, R.drawable.emoji_1f4c2); sEmojisMap.append(0x1f4c3, R.drawable.emoji_1f4c3); sEmojisMap.append(0x1f4c4, R.drawable.emoji_1f4c4); sEmojisMap.append(0x1f4c5, R.drawable.emoji_1f4c5); sEmojisMap.append(0x1f4c6, R.drawable.emoji_1f4c6); sEmojisMap.append(0x1f4c7, R.drawable.emoji_1f4c7); sEmojisMap.append(0x1f4c8, R.drawable.emoji_1f4c8); sEmojisMap.append(0x1f4c9, R.drawable.emoji_1f4c9); sEmojisMap.append(0x1f4ca, R.drawable.emoji_1f4ca); sEmojisMap.append(0x1f4cb, R.drawable.emoji_1f4cb); sEmojisMap.append(0x1f4cc, R.drawable.emoji_1f4cc); sEmojisMap.append(0x1f4cd, R.drawable.emoji_1f4cd); sEmojisMap.append(0x1f4ce, R.drawable.emoji_1f4ce); sEmojisMap.append(0x1f4cf, R.drawable.emoji_1f4cf); sEmojisMap.append(0x1f4d0, R.drawable.emoji_1f4d0); sEmojisMap.append(0x1f4d1, R.drawable.emoji_1f4d1); sEmojisMap.append(0x1f4d2, R.drawable.emoji_1f4d2); sEmojisMap.append(0x1f4d3, R.drawable.emoji_1f4d3); sEmojisMap.append(0x1f4d4, R.drawable.emoji_1f4d4); sEmojisMap.append(0x1f4d5, R.drawable.emoji_1f4d5); sEmojisMap.append(0x1f4d6, R.drawable.emoji_1f4d6); sEmojisMap.append(0x1f4d7, R.drawable.emoji_1f4d7); sEmojisMap.append(0x1f4d8, R.drawable.emoji_1f4d8); sEmojisMap.append(0x1f4d9, R.drawable.emoji_1f4d9); sEmojisMap.append(0x1f4da, R.drawable.emoji_1f4da); sEmojisMap.append(0x1f4db, R.drawable.emoji_1f4db); sEmojisMap.append(0x1f4dc, R.drawable.emoji_1f4dc); sEmojisMap.append(0x1f4dd, R.drawable.emoji_1f4dd); sEmojisMap.append(0x1f4de, R.drawable.emoji_1f4de); sEmojisMap.append(0x1f4df, R.drawable.emoji_1f4df); sEmojisMap.append(0x1f4e0, R.drawable.emoji_1f4e0); sEmojisMap.append(0x1f4e1, R.drawable.emoji_1f4e1); sEmojisMap.append(0x1f4e2, R.drawable.emoji_1f4e2); sEmojisMap.append(0x1f4e3, R.drawable.emoji_1f4e3); sEmojisMap.append(0x1f4e4, R.drawable.emoji_1f4e4); sEmojisMap.append(0x1f4e5, R.drawable.emoji_1f4e5); sEmojisMap.append(0x1f4e6, R.drawable.emoji_1f4e6); sEmojisMap.append(0x1f4e7, R.drawable.emoji_1f4e7); sEmojisMap.append(0x1f4e8, R.drawable.emoji_1f4e8); sEmojisMap.append(0x1f4e9, R.drawable.emoji_1f4e9); sEmojisMap.append(0x1f4ea, R.drawable.emoji_1f4ea); sEmojisMap.append(0x1f4eb, R.drawable.emoji_1f4eb); sEmojisMap.append(0x1f4ec, R.drawable.emoji_1f4ec); sEmojisMap.append(0x1f4ed, R.drawable.emoji_1f4ed); sEmojisMap.append(0x1f4ee, R.drawable.emoji_1f4ee); sEmojisMap.append(0x1f4ef, R.drawable.emoji_1f4ef); sEmojisMap.append(0x1f4f0, R.drawable.emoji_1f4f0); sEmojisMap.append(0x1f4f1, R.drawable.emoji_1f4f1); sEmojisMap.append(0x1f4f2, R.drawable.emoji_1f4f2); sEmojisMap.append(0x1f4f3, R.drawable.emoji_1f4f3); sEmojisMap.append(0x1f4f4, R.drawable.emoji_1f4f4); sEmojisMap.append(0x1f4f5, R.drawable.emoji_1f4f5); sEmojisMap.append(0x1f4f6, R.drawable.emoji_1f4f6); sEmojisMap.append(0x1f4f7, R.drawable.emoji_1f4f7); sEmojisMap.append(0x1f4f9, R.drawable.emoji_1f4f9); sEmojisMap.append(0x1f4fa, R.drawable.emoji_1f4fa); sEmojisMap.append(0x1f4fb, R.drawable.emoji_1f4fb); sEmojisMap.append(0x1f4fc, R.drawable.emoji_1f4fc); sEmojisMap.append(0x1f500, R.drawable.emoji_1f500); sEmojisMap.append(0x1f501, R.drawable.emoji_1f501); sEmojisMap.append(0x1f502, R.drawable.emoji_1f502); sEmojisMap.append(0x1f503, R.drawable.emoji_1f503); sEmojisMap.append(0x1f504, R.drawable.emoji_1f504); sEmojisMap.append(0x1f505, R.drawable.emoji_1f505); sEmojisMap.append(0x1f506, R.drawable.emoji_1f506); sEmojisMap.append(0x1f507, R.drawable.emoji_1f507); sEmojisMap.append(0x1f508, R.drawable.emoji_1f508); sEmojisMap.append(0x1f509, R.drawable.emoji_1f509); sEmojisMap.append(0x1f50a, R.drawable.emoji_1f50a); sEmojisMap.append(0x1f50b, R.drawable.emoji_1f50b); sEmojisMap.append(0x1f50c, R.drawable.emoji_1f50c); sEmojisMap.append(0x1f50d, R.drawable.emoji_1f50d); sEmojisMap.append(0x1f50e, R.drawable.emoji_1f50e); sEmojisMap.append(0x1f50f, R.drawable.emoji_1f50f); sEmojisMap.append(0x1f510, R.drawable.emoji_1f510); sEmojisMap.append(0x1f511, R.drawable.emoji_1f511); sEmojisMap.append(0x1f512, R.drawable.emoji_1f512); sEmojisMap.append(0x1f513, R.drawable.emoji_1f513); sEmojisMap.append(0x1f514, R.drawable.emoji_1f514); sEmojisMap.append(0x1f515, R.drawable.emoji_1f515); sEmojisMap.append(0x1f516, R.drawable.emoji_1f516); sEmojisMap.append(0x1f517, R.drawable.emoji_1f517); sEmojisMap.append(0x1f518, R.drawable.emoji_1f518); sEmojisMap.append(0x1f519, R.drawable.emoji_1f519); sEmojisMap.append(0x1f51a, R.drawable.emoji_1f51a); sEmojisMap.append(0x1f51b, R.drawable.emoji_1f51b); sEmojisMap.append(0x1f51c, R.drawable.emoji_1f51c); sEmojisMap.append(0x1f51d, R.drawable.emoji_1f51d); sEmojisMap.append(0x1f51e, R.drawable.emoji_1f51e); sEmojisMap.append(0x1f51f, R.drawable.emoji_1f51f); sEmojisMap.append(0x1f520, R.drawable.emoji_1f520); sEmojisMap.append(0x1f521, R.drawable.emoji_1f521); sEmojisMap.append(0x1f522, R.drawable.emoji_1f522); sEmojisMap.append(0x1f523, R.drawable.emoji_1f523); sEmojisMap.append(0x1f524, R.drawable.emoji_1f524); sEmojisMap.append(0x1f525, R.drawable.emoji_1f525); sEmojisMap.append(0x1f526, R.drawable.emoji_1f526); sEmojisMap.append(0x1f527, R.drawable.emoji_1f527); sEmojisMap.append(0x1f528, R.drawable.emoji_1f528); sEmojisMap.append(0x1f529, R.drawable.emoji_1f529); sEmojisMap.append(0x1f52a, R.drawable.emoji_1f52a); sEmojisMap.append(0x1f52b, R.drawable.emoji_1f52b); sEmojisMap.append(0x1f52c, R.drawable.emoji_1f52c); sEmojisMap.append(0x1f52d, R.drawable.emoji_1f52d); sEmojisMap.append(0x1f52e, R.drawable.emoji_1f52e); sEmojisMap.append(0x1f52f, R.drawable.emoji_1f52f); sEmojisMap.append(0x1f530, R.drawable.emoji_1f530); sEmojisMap.append(0x1f531, R.drawable.emoji_1f531); sEmojisMap.append(0x1f532, R.drawable.emoji_1f532); sEmojisMap.append(0x1f533, R.drawable.emoji_1f533); sEmojisMap.append(0x1f534, R.drawable.emoji_1f534); sEmojisMap.append(0x1f535, R.drawable.emoji_1f535); sEmojisMap.append(0x1f536, R.drawable.emoji_1f536); sEmojisMap.append(0x1f537, R.drawable.emoji_1f537); sEmojisMap.append(0x1f538, R.drawable.emoji_1f538); sEmojisMap.append(0x1f539, R.drawable.emoji_1f539); sEmojisMap.append(0x1f53a, R.drawable.emoji_1f53a); sEmojisMap.append(0x1f53b, R.drawable.emoji_1f53b); sEmojisMap.append(0x1f53c, R.drawable.emoji_1f53c); sEmojisMap.append(0x1f53d, R.drawable.emoji_1f53d); sEmojisMap.append(0x1f550, R.drawable.emoji_1f550); sEmojisMap.append(0x1f551, R.drawable.emoji_1f551); sEmojisMap.append(0x1f552, R.drawable.emoji_1f552); sEmojisMap.append(0x1f553, R.drawable.emoji_1f553); sEmojisMap.append(0x1f554, R.drawable.emoji_1f554); sEmojisMap.append(0x1f555, R.drawable.emoji_1f555); sEmojisMap.append(0x1f556, R.drawable.emoji_1f556); sEmojisMap.append(0x1f557, R.drawable.emoji_1f557); sEmojisMap.append(0x1f558, R.drawable.emoji_1f558); sEmojisMap.append(0x1f559, R.drawable.emoji_1f559); sEmojisMap.append(0x1f55a, R.drawable.emoji_1f55a); sEmojisMap.append(0x1f55b, R.drawable.emoji_1f55b); sEmojisMap.append(0x1f55c, R.drawable.emoji_1f55c); sEmojisMap.append(0x1f55d, R.drawable.emoji_1f55d); sEmojisMap.append(0x1f55e, R.drawable.emoji_1f55e); sEmojisMap.append(0x1f55f, R.drawable.emoji_1f55f); sEmojisMap.append(0x1f560, R.drawable.emoji_1f560); sEmojisMap.append(0x1f561, R.drawable.emoji_1f561); sEmojisMap.append(0x1f562, R.drawable.emoji_1f562); sEmojisMap.append(0x1f563, R.drawable.emoji_1f563); sEmojisMap.append(0x1f564, R.drawable.emoji_1f564); sEmojisMap.append(0x1f565, R.drawable.emoji_1f565); sEmojisMap.append(0x1f566, R.drawable.emoji_1f566); sEmojisMap.append(0x1f567, R.drawable.emoji_1f567); sEmojisMap.append(0x1f5fb, R.drawable.emoji_1f5fb); sEmojisMap.append(0x1f5fc, R.drawable.emoji_1f5fc); sEmojisMap.append(0x1f5fd, R.drawable.emoji_1f5fd); sEmojisMap.append(0x1f5fe, R.drawable.emoji_1f5fe); sEmojisMap.append(0x1f5ff, R.drawable.emoji_1f5ff); sEmojisMap.append(0x1f600, R.drawable.emoji_1f600); sEmojisMap.append(0x1f601, R.drawable.emoji_1f601); sEmojisMap.append(0x1f602, R.drawable.emoji_1f602); sEmojisMap.append(0x1f603, R.drawable.emoji_1f603); sEmojisMap.append(0x1f604, R.drawable.emoji_1f604); sEmojisMap.append(0x1f605, R.drawable.emoji_1f605); sEmojisMap.append(0x1f606, R.drawable.emoji_1f606); sEmojisMap.append(0x1f607, R.drawable.emoji_1f607); sEmojisMap.append(0x1f608, R.drawable.emoji_1f608); sEmojisMap.append(0x1f609, R.drawable.emoji_1f609); sEmojisMap.append(0x1f60a, R.drawable.emoji_1f60a); sEmojisMap.append(0x1f60b, R.drawable.emoji_1f60b); sEmojisMap.append(0x1f60c, R.drawable.emoji_1f60c); sEmojisMap.append(0x1f60d, R.drawable.emoji_1f60d); sEmojisMap.append(0x1f60e, R.drawable.emoji_1f60e); sEmojisMap.append(0x1f60f, R.drawable.emoji_1f60f); sEmojisMap.append(0x1f610, R.drawable.emoji_1f610); sEmojisMap.append(0x1f611, R.drawable.emoji_1f611); sEmojisMap.append(0x1f612, R.drawable.emoji_1f612); sEmojisMap.append(0x1f613, R.drawable.emoji_1f613); sEmojisMap.append(0x1f614, R.drawable.emoji_1f614); sEmojisMap.append(0x1f615, R.drawable.emoji_1f615); sEmojisMap.append(0x1f616, R.drawable.emoji_1f616); sEmojisMap.append(0x1f617, R.drawable.emoji_1f617); sEmojisMap.append(0x1f618, R.drawable.emoji_1f618); sEmojisMap.append(0x1f619, R.drawable.emoji_1f619); sEmojisMap.append(0x1f61a, R.drawable.emoji_1f61a); sEmojisMap.append(0x1f61b, R.drawable.emoji_1f61b); sEmojisMap.append(0x1f61c, R.drawable.emoji_1f61c); sEmojisMap.append(0x1f61d, R.drawable.emoji_1f61d); sEmojisMap.append(0x1f61e, R.drawable.emoji_1f61e); sEmojisMap.append(0x1f61f, R.drawable.emoji_1f61f); sEmojisMap.append(0x1f620, R.drawable.emoji_1f620); sEmojisMap.append(0x1f621, R.drawable.emoji_1f621); sEmojisMap.append(0x1f622, R.drawable.emoji_1f622); sEmojisMap.append(0x1f623, R.drawable.emoji_1f623); sEmojisMap.append(0x1f624, R.drawable.emoji_1f624); sEmojisMap.append(0x1f625, R.drawable.emoji_1f625); sEmojisMap.append(0x1f626, R.drawable.emoji_1f626); sEmojisMap.append(0x1f627, R.drawable.emoji_1f627); sEmojisMap.append(0x1f628, R.drawable.emoji_1f628); sEmojisMap.append(0x1f629, R.drawable.emoji_1f629); sEmojisMap.append(0x1f62a, R.drawable.emoji_1f62a); sEmojisMap.append(0x1f62b, R.drawable.emoji_1f62b); sEmojisMap.append(0x1f62c, R.drawable.emoji_1f62c); sEmojisMap.append(0x1f62d, R.drawable.emoji_1f62d); sEmojisMap.append(0x1f62e, R.drawable.emoji_1f62e); sEmojisMap.append(0x1f62f, R.drawable.emoji_1f62f); sEmojisMap.append(0x1f630, R.drawable.emoji_1f630); sEmojisMap.append(0x1f631, R.drawable.emoji_1f631); sEmojisMap.append(0x1f632, R.drawable.emoji_1f632); sEmojisMap.append(0x1f633, R.drawable.emoji_1f633); sEmojisMap.append(0x1f634, R.drawable.emoji_1f634); sEmojisMap.append(0x1f635, R.drawable.emoji_1f635); sEmojisMap.append(0x1f636, R.drawable.emoji_1f636); sEmojisMap.append(0x1f637, R.drawable.emoji_1f637); sEmojisMap.append(0x1f638, R.drawable.emoji_1f638); sEmojisMap.append(0x1f639, R.drawable.emoji_1f639); sEmojisMap.append(0x1f63a, R.drawable.emoji_1f63a); sEmojisMap.append(0x1f63b, R.drawable.emoji_1f63b); sEmojisMap.append(0x1f63c, R.drawable.emoji_1f63c); sEmojisMap.append(0x1f63d, R.drawable.emoji_1f63d); sEmojisMap.append(0x1f63e, R.drawable.emoji_1f63e); sEmojisMap.append(0x1f63f, R.drawable.emoji_1f63f); sEmojisMap.append(0x1f640, R.drawable.emoji_1f640); sEmojisMap.append(0x1f645, R.drawable.emoji_1f645); sEmojisMap.append(0x1f646, R.drawable.emoji_1f646); sEmojisMap.append(0x1f647, R.drawable.emoji_1f647); sEmojisMap.append(0x1f648, R.drawable.emoji_1f648); sEmojisMap.append(0x1f649, R.drawable.emoji_1f649); sEmojisMap.append(0x1f64a, R.drawable.emoji_1f64a); sEmojisMap.append(0x1f64b, R.drawable.emoji_1f64b); sEmojisMap.append(0x1f64c, R.drawable.emoji_1f64c); sEmojisMap.append(0x1f64d, R.drawable.emoji_1f64d); sEmojisMap.append(0x1f64e, R.drawable.emoji_1f64e); sEmojisMap.append(0x1f64f, R.drawable.emoji_1f64f); sEmojisMap.append(0x1f680, R.drawable.emoji_1f680); sEmojisMap.append(0x1f681, R.drawable.emoji_1f681); sEmojisMap.append(0x1f682, R.drawable.emoji_1f682); sEmojisMap.append(0x1f683, R.drawable.emoji_1f683); sEmojisMap.append(0x1f684, R.drawable.emoji_1f684); sEmojisMap.append(0x1f685, R.drawable.emoji_1f685); sEmojisMap.append(0x1f686, R.drawable.emoji_1f686); sEmojisMap.append(0x1f687, R.drawable.emoji_1f687); sEmojisMap.append(0x1f688, R.drawable.emoji_1f688); sEmojisMap.append(0x1f689, R.drawable.emoji_1f689); sEmojisMap.append(0x1f68a, R.drawable.emoji_1f68a); sEmojisMap.append(0x1f68b, R.drawable.emoji_1f68b); sEmojisMap.append(0x1f68c, R.drawable.emoji_1f68c); sEmojisMap.append(0x1f68d, R.drawable.emoji_1f68d); sEmojisMap.append(0x1f68e, R.drawable.emoji_1f68e); sEmojisMap.append(0x1f68f, R.drawable.emoji_1f68f); sEmojisMap.append(0x1f690, R.drawable.emoji_1f690); sEmojisMap.append(0x1f691, R.drawable.emoji_1f691); sEmojisMap.append(0x1f692, R.drawable.emoji_1f692); sEmojisMap.append(0x1f693, R.drawable.emoji_1f693); sEmojisMap.append(0x1f694, R.drawable.emoji_1f694); sEmojisMap.append(0x1f695, R.drawable.emoji_1f695); sEmojisMap.append(0x1f696, R.drawable.emoji_1f696); sEmojisMap.append(0x1f697, R.drawable.emoji_1f697); sEmojisMap.append(0x1f698, R.drawable.emoji_1f698); sEmojisMap.append(0x1f699, R.drawable.emoji_1f699); sEmojisMap.append(0x1f69a, R.drawable.emoji_1f69a); sEmojisMap.append(0x1f69b, R.drawable.emoji_1f69b); sEmojisMap.append(0x1f69c, R.drawable.emoji_1f69c); sEmojisMap.append(0x1f69d, R.drawable.emoji_1f69d); sEmojisMap.append(0x1f69e, R.drawable.emoji_1f69e); sEmojisMap.append(0x1f69f, R.drawable.emoji_1f69f); sEmojisMap.append(0x1f6a0, R.drawable.emoji_1f6a0); sEmojisMap.append(0x1f6a1, R.drawable.emoji_1f6a1); sEmojisMap.append(0x1f6a2, R.drawable.emoji_1f6a2); sEmojisMap.append(0x1f6a3, R.drawable.emoji_1f6a3); sEmojisMap.append(0x1f6a4, R.drawable.emoji_1f6a4); sEmojisMap.append(0x1f6a5, R.drawable.emoji_1f6a5); sEmojisMap.append(0x1f6a6, R.drawable.emoji_1f6a6); sEmojisMap.append(0x1f6a7, R.drawable.emoji_1f6a7); sEmojisMap.append(0x1f6a8, R.drawable.emoji_1f6a8); sEmojisMap.append(0x1f6a9, R.drawable.emoji_1f6a9); sEmojisMap.append(0x1f6aa, R.drawable.emoji_1f6aa); sEmojisMap.append(0x1f6ab, R.drawable.emoji_1f6ab); sEmojisMap.append(0x1f6ac, R.drawable.emoji_1f6ac); sEmojisMap.append(0x1f6ad, R.drawable.emoji_1f6ad); sEmojisMap.append(0x1f6ae, R.drawable.emoji_1f6ae); sEmojisMap.append(0x1f6af, R.drawable.emoji_1f6af); sEmojisMap.append(0x1f6b0, R.drawable.emoji_1f6b0); sEmojisMap.append(0x1f6b1, R.drawable.emoji_1f6b1); sEmojisMap.append(0x1f6b2, R.drawable.emoji_1f6b2); sEmojisMap.append(0x1f6b3, R.drawable.emoji_1f6b3); sEmojisMap.append(0x1f6b4, R.drawable.emoji_1f6b4); sEmojisMap.append(0x1f6b5, R.drawable.emoji_1f6b5); sEmojisMap.append(0x1f6b6, R.drawable.emoji_1f6b6); sEmojisMap.append(0x1f6b7, R.drawable.emoji_1f6b7); sEmojisMap.append(0x1f6b8, R.drawable.emoji_1f6b8); sEmojisMap.append(0x1f6b9, R.drawable.emoji_1f6b9); sEmojisMap.append(0x1f6ba, R.drawable.emoji_1f6ba); sEmojisMap.append(0x1f6bb, R.drawable.emoji_1f6bb); sEmojisMap.append(0x1f6bc, R.drawable.emoji_1f6bc); sEmojisMap.append(0x1f6bd, R.drawable.emoji_1f6bd); sEmojisMap.append(0x1f6be, R.drawable.emoji_1f6be); sEmojisMap.append(0x1f6bf, R.drawable.emoji_1f6bf); sEmojisMap.append(0x1f6c0, R.drawable.emoji_1f6c0); sEmojisMap.append(0x1f6c1, R.drawable.emoji_1f6c1); sEmojisMap.append(0x1f6c2, R.drawable.emoji_1f6c2); sEmojisMap.append(0x1f6c3, R.drawable.emoji_1f6c3); sEmojisMap.append(0x1f6c4, R.drawable.emoji_1f6c4); sEmojisMap.append(0x1f6c5, R.drawable.emoji_1f6c5); sSoftbanksMap.append(0xe001, R.drawable.emoji_1f466); sSoftbanksMap.append(0xe002, R.drawable.emoji_1f467); sSoftbanksMap.append(0xe003, R.drawable.emoji_1f48b); sSoftbanksMap.append(0xe004, R.drawable.emoji_1f468); sSoftbanksMap.append(0xe005, R.drawable.emoji_1f469); sSoftbanksMap.append(0xe006, R.drawable.emoji_1f455); sSoftbanksMap.append(0xe007, R.drawable.emoji_1f45e); sSoftbanksMap.append(0xe008, R.drawable.emoji_1f4f7); sSoftbanksMap.append(0xe009, R.drawable.emoji_1f4de); sSoftbanksMap.append(0xe00a, R.drawable.emoji_1f4f1); sSoftbanksMap.append(0xe00b, R.drawable.emoji_1f4e0); sSoftbanksMap.append(0xe00c, R.drawable.emoji_1f4bb); sSoftbanksMap.append(0xe00d, R.drawable.emoji_1f44a); sSoftbanksMap.append(0xe00e, R.drawable.emoji_1f44d); sSoftbanksMap.append(0xe00f, R.drawable.emoji_261d); sSoftbanksMap.append(0xe010, R.drawable.emoji_270a); sSoftbanksMap.append(0xe011, R.drawable.emoji_270c); sSoftbanksMap.append(0xe012, R.drawable.emoji_1f64b); sSoftbanksMap.append(0xe013, R.drawable.emoji_1f3bf); sSoftbanksMap.append(0xe014, R.drawable.emoji_26f3); sSoftbanksMap.append(0xe015, R.drawable.emoji_1f3be); sSoftbanksMap.append(0xe016, R.drawable.emoji_26be); sSoftbanksMap.append(0xe017, R.drawable.emoji_1f3c4); sSoftbanksMap.append(0xe018, R.drawable.emoji_26bd); sSoftbanksMap.append(0xe019, R.drawable.emoji_1f3a3); sSoftbanksMap.append(0xe01a, R.drawable.emoji_1f434); sSoftbanksMap.append(0xe01b, R.drawable.emoji_1f697); sSoftbanksMap.append(0xe01c, R.drawable.emoji_26f5); sSoftbanksMap.append(0xe01d, R.drawable.emoji_2708); sSoftbanksMap.append(0xe01e, R.drawable.emoji_1f683); sSoftbanksMap.append(0xe01f, R.drawable.emoji_1f685); sSoftbanksMap.append(0xe020, R.drawable.emoji_2753); sSoftbanksMap.append(0xe021, R.drawable.emoji_2757); sSoftbanksMap.append(0xe022, R.drawable.emoji_2764); sSoftbanksMap.append(0xe023, R.drawable.emoji_1f494); sSoftbanksMap.append(0xe024, R.drawable.emoji_1f550); sSoftbanksMap.append(0xe025, R.drawable.emoji_1f551); sSoftbanksMap.append(0xe026, R.drawable.emoji_1f552); sSoftbanksMap.append(0xe027, R.drawable.emoji_1f553); sSoftbanksMap.append(0xe028, R.drawable.emoji_1f554); sSoftbanksMap.append(0xe029, R.drawable.emoji_1f555); sSoftbanksMap.append(0xe02a, R.drawable.emoji_1f556); sSoftbanksMap.append(0xe02b, R.drawable.emoji_1f557); sSoftbanksMap.append(0xe02c, R.drawable.emoji_1f558); sSoftbanksMap.append(0xe02d, R.drawable.emoji_1f559); sSoftbanksMap.append(0xe02e, R.drawable.emoji_1f55a); sSoftbanksMap.append(0xe02f, R.drawable.emoji_1f55b); sSoftbanksMap.append(0xe030, R.drawable.emoji_1f338); sSoftbanksMap.append(0xe031, R.drawable.emoji_1f531); sSoftbanksMap.append(0xe032, R.drawable.emoji_1f339); sSoftbanksMap.append(0xe033, R.drawable.emoji_1f384); sSoftbanksMap.append(0xe034, R.drawable.emoji_1f48d); sSoftbanksMap.append(0xe035, R.drawable.emoji_1f48e); sSoftbanksMap.append(0xe036, R.drawable.emoji_1f3e0); sSoftbanksMap.append(0xe037, R.drawable.emoji_26ea); sSoftbanksMap.append(0xe038, R.drawable.emoji_1f3e2); sSoftbanksMap.append(0xe039, R.drawable.emoji_1f689); sSoftbanksMap.append(0xe03a, R.drawable.emoji_26fd); sSoftbanksMap.append(0xe03b, R.drawable.emoji_1f5fb); sSoftbanksMap.append(0xe03c, R.drawable.emoji_1f3a4); sSoftbanksMap.append(0xe03d, R.drawable.emoji_1f3a5); sSoftbanksMap.append(0xe03e, R.drawable.emoji_1f3b5); sSoftbanksMap.append(0xe03f, R.drawable.emoji_1f511); sSoftbanksMap.append(0xe040, R.drawable.emoji_1f3b7); sSoftbanksMap.append(0xe041, R.drawable.emoji_1f3b8); sSoftbanksMap.append(0xe042, R.drawable.emoji_1f3ba); sSoftbanksMap.append(0xe043, R.drawable.emoji_1f374); sSoftbanksMap.append(0xe044, R.drawable.emoji_1f377); sSoftbanksMap.append(0xe045, R.drawable.emoji_2615); sSoftbanksMap.append(0xe046, R.drawable.emoji_1f370); sSoftbanksMap.append(0xe047, R.drawable.emoji_1f37a); sSoftbanksMap.append(0xe048, R.drawable.emoji_26c4); sSoftbanksMap.append(0xe049, R.drawable.emoji_2601); sSoftbanksMap.append(0xe04a, R.drawable.emoji_2600); sSoftbanksMap.append(0xe04b, R.drawable.emoji_2614); sSoftbanksMap.append(0xe04c, R.drawable.emoji_1f313); sSoftbanksMap.append(0xe04d, R.drawable.emoji_1f304); sSoftbanksMap.append(0xe04e, R.drawable.emoji_1f47c); sSoftbanksMap.append(0xe04f, R.drawable.emoji_1f431); sSoftbanksMap.append(0xe050, R.drawable.emoji_1f42f); sSoftbanksMap.append(0xe051, R.drawable.emoji_1f43b); sSoftbanksMap.append(0xe052, R.drawable.emoji_1f429); sSoftbanksMap.append(0xe053, R.drawable.emoji_1f42d); sSoftbanksMap.append(0xe054, R.drawable.emoji_1f433); sSoftbanksMap.append(0xe055, R.drawable.emoji_1f427); sSoftbanksMap.append(0xe056, R.drawable.emoji_1f60a); sSoftbanksMap.append(0xe057, R.drawable.emoji_1f603); sSoftbanksMap.append(0xe058, R.drawable.emoji_1f61e); sSoftbanksMap.append(0xe059, R.drawable.emoji_1f620); sSoftbanksMap.append(0xe05a, R.drawable.emoji_1f4a9); sSoftbanksMap.append(0xe101, R.drawable.emoji_1f4ea); sSoftbanksMap.append(0xe102, R.drawable.emoji_1f4ee); sSoftbanksMap.append(0xe103, R.drawable.emoji_1f4e7); sSoftbanksMap.append(0xe104, R.drawable.emoji_1f4f2); sSoftbanksMap.append(0xe105, R.drawable.emoji_1f61c); sSoftbanksMap.append(0xe106, R.drawable.emoji_1f60d); sSoftbanksMap.append(0xe107, R.drawable.emoji_1f631); sSoftbanksMap.append(0xe108, R.drawable.emoji_1f613); sSoftbanksMap.append(0xe109, R.drawable.emoji_1f435); sSoftbanksMap.append(0xe10a, R.drawable.emoji_1f419); sSoftbanksMap.append(0xe10b, R.drawable.emoji_1f437); sSoftbanksMap.append(0xe10c, R.drawable.emoji_1f47d); sSoftbanksMap.append(0xe10d, R.drawable.emoji_1f680); sSoftbanksMap.append(0xe10e, R.drawable.emoji_1f451); sSoftbanksMap.append(0xe10f, R.drawable.emoji_1f4a1); sSoftbanksMap.append(0xe110, R.drawable.emoji_1f331); sSoftbanksMap.append(0xe111, R.drawable.emoji_1f48f); sSoftbanksMap.append(0xe112, R.drawable.emoji_1f381); sSoftbanksMap.append(0xe113, R.drawable.emoji_1f52b); sSoftbanksMap.append(0xe114, R.drawable.emoji_1f50d); sSoftbanksMap.append(0xe115, R.drawable.emoji_1f3c3); sSoftbanksMap.append(0xe116, R.drawable.emoji_1f528); sSoftbanksMap.append(0xe117, R.drawable.emoji_1f386); sSoftbanksMap.append(0xe118, R.drawable.emoji_1f341); sSoftbanksMap.append(0xe119, R.drawable.emoji_1f342); sSoftbanksMap.append(0xe11a, R.drawable.emoji_1f47f); sSoftbanksMap.append(0xe11b, R.drawable.emoji_1f47b); sSoftbanksMap.append(0xe11c, R.drawable.emoji_1f480); sSoftbanksMap.append(0xe11d, R.drawable.emoji_1f525); sSoftbanksMap.append(0xe11e, R.drawable.emoji_1f4bc); sSoftbanksMap.append(0xe11f, R.drawable.emoji_1f4ba); sSoftbanksMap.append(0xe120, R.drawable.emoji_1f354); sSoftbanksMap.append(0xe121, R.drawable.emoji_26f2); sSoftbanksMap.append(0xe122, R.drawable.emoji_26fa); sSoftbanksMap.append(0xe123, R.drawable.emoji_2668); sSoftbanksMap.append(0xe124, R.drawable.emoji_1f3a1); sSoftbanksMap.append(0xe125, R.drawable.emoji_1f3ab); sSoftbanksMap.append(0xe126, R.drawable.emoji_1f4bf); sSoftbanksMap.append(0xe127, R.drawable.emoji_1f4c0); sSoftbanksMap.append(0xe128, R.drawable.emoji_1f4fb); sSoftbanksMap.append(0xe129, R.drawable.emoji_1f4fc); sSoftbanksMap.append(0xe12a, R.drawable.emoji_1f4fa); sSoftbanksMap.append(0xe12b, R.drawable.emoji_1f47e); sSoftbanksMap.append(0xe12c, R.drawable.emoji_303d); sSoftbanksMap.append(0xe12d, R.drawable.emoji_1f004); sSoftbanksMap.append(0xe12e, R.drawable.emoji_1f19a); sSoftbanksMap.append(0xe12f, R.drawable.emoji_1f4b0); sSoftbanksMap.append(0xe130, R.drawable.emoji_1f3af); sSoftbanksMap.append(0xe131, R.drawable.emoji_1f3c6); sSoftbanksMap.append(0xe132, R.drawable.emoji_1f3c1); sSoftbanksMap.append(0xe133, R.drawable.emoji_1f3b0); sSoftbanksMap.append(0xe134, R.drawable.emoji_1f40e); sSoftbanksMap.append(0xe135, R.drawable.emoji_1f6a4); sSoftbanksMap.append(0xe136, R.drawable.emoji_1f6b2); sSoftbanksMap.append(0xe137, R.drawable.emoji_1f6a7); sSoftbanksMap.append(0xe138, R.drawable.emoji_1f6b9); sSoftbanksMap.append(0xe139, R.drawable.emoji_1f6ba); sSoftbanksMap.append(0xe13a, R.drawable.emoji_1f6bc); sSoftbanksMap.append(0xe13b, R.drawable.emoji_1f489); sSoftbanksMap.append(0xe13c, R.drawable.emoji_1f4a4); sSoftbanksMap.append(0xe13d, R.drawable.emoji_26a1); sSoftbanksMap.append(0xe13e, R.drawable.emoji_1f460); sSoftbanksMap.append(0xe13f, R.drawable.emoji_1f6c0); sSoftbanksMap.append(0xe140, R.drawable.emoji_1f6bd); sSoftbanksMap.append(0xe141, R.drawable.emoji_1f50a); sSoftbanksMap.append(0xe142, R.drawable.emoji_1f4e2); sSoftbanksMap.append(0xe143, R.drawable.emoji_1f38c); sSoftbanksMap.append(0xe144, R.drawable.emoji_1f50f); sSoftbanksMap.append(0xe145, R.drawable.emoji_1f513); sSoftbanksMap.append(0xe146, R.drawable.emoji_1f306); sSoftbanksMap.append(0xe147, R.drawable.emoji_1f373); sSoftbanksMap.append(0xe148, R.drawable.emoji_1f4c7); sSoftbanksMap.append(0xe149, R.drawable.emoji_1f4b1); sSoftbanksMap.append(0xe14a, R.drawable.emoji_1f4b9); sSoftbanksMap.append(0xe14b, R.drawable.emoji_1f4e1); sSoftbanksMap.append(0xe14c, R.drawable.emoji_1f4aa); sSoftbanksMap.append(0xe14d, R.drawable.emoji_1f3e6); sSoftbanksMap.append(0xe14e, R.drawable.emoji_1f6a5); sSoftbanksMap.append(0xe14f, R.drawable.emoji_1f17f); sSoftbanksMap.append(0xe150, R.drawable.emoji_1f68f); sSoftbanksMap.append(0xe151, R.drawable.emoji_1f6bb); sSoftbanksMap.append(0xe152, R.drawable.emoji_1f46e); sSoftbanksMap.append(0xe153, R.drawable.emoji_1f3e3); sSoftbanksMap.append(0xe154, R.drawable.emoji_1f3e7); sSoftbanksMap.append(0xe155, R.drawable.emoji_1f3e5); sSoftbanksMap.append(0xe156, R.drawable.emoji_1f3ea); sSoftbanksMap.append(0xe157, R.drawable.emoji_1f3eb); sSoftbanksMap.append(0xe158, R.drawable.emoji_1f3e8); sSoftbanksMap.append(0xe159, R.drawable.emoji_1f68c); sSoftbanksMap.append(0xe15a, R.drawable.emoji_1f695); sSoftbanksMap.append(0xe201, R.drawable.emoji_1f6b6); sSoftbanksMap.append(0xe202, R.drawable.emoji_1f6a2); sSoftbanksMap.append(0xe203, R.drawable.emoji_1f201); sSoftbanksMap.append(0xe204, R.drawable.emoji_1f49f); sSoftbanksMap.append(0xe205, R.drawable.emoji_2734); sSoftbanksMap.append(0xe206, R.drawable.emoji_2733); sSoftbanksMap.append(0xe207, R.drawable.emoji_1f51e); sSoftbanksMap.append(0xe208, R.drawable.emoji_1f6ad); sSoftbanksMap.append(0xe209, R.drawable.emoji_1f530); sSoftbanksMap.append(0xe20a, R.drawable.emoji_267f); sSoftbanksMap.append(0xe20b, R.drawable.emoji_1f4f6); sSoftbanksMap.append(0xe20c, R.drawable.emoji_2665); sSoftbanksMap.append(0xe20d, R.drawable.emoji_2666); sSoftbanksMap.append(0xe20e, R.drawable.emoji_2660); sSoftbanksMap.append(0xe20f, R.drawable.emoji_2663); sSoftbanksMap.append(0xe210, R.drawable.emoji_0023); sSoftbanksMap.append(0xe211, R.drawable.emoji_27bf); sSoftbanksMap.append(0xe212, R.drawable.emoji_1f195); sSoftbanksMap.append(0xe213, R.drawable.emoji_1f199); sSoftbanksMap.append(0xe214, R.drawable.emoji_1f192); sSoftbanksMap.append(0xe215, R.drawable.emoji_1f236); sSoftbanksMap.append(0xe216, R.drawable.emoji_1f21a); sSoftbanksMap.append(0xe217, R.drawable.emoji_1f237); sSoftbanksMap.append(0xe218, R.drawable.emoji_1f238); sSoftbanksMap.append(0xe219, R.drawable.emoji_1f534); sSoftbanksMap.append(0xe21a, R.drawable.emoji_1f532); sSoftbanksMap.append(0xe21b, R.drawable.emoji_1f533); sSoftbanksMap.append(0xe21c, R.drawable.emoji_0031); sSoftbanksMap.append(0xe21d, R.drawable.emoji_0032); sSoftbanksMap.append(0xe21e, R.drawable.emoji_0033); sSoftbanksMap.append(0xe21f, R.drawable.emoji_0034); sSoftbanksMap.append(0xe220, R.drawable.emoji_0035); sSoftbanksMap.append(0xe221, R.drawable.emoji_0036); sSoftbanksMap.append(0xe222, R.drawable.emoji_0037); sSoftbanksMap.append(0xe223, R.drawable.emoji_0038); sSoftbanksMap.append(0xe224, R.drawable.emoji_0039); sSoftbanksMap.append(0xe225, R.drawable.emoji_0030); sSoftbanksMap.append(0xe226, R.drawable.emoji_1f250); sSoftbanksMap.append(0xe227, R.drawable.emoji_1f239); sSoftbanksMap.append(0xe228, R.drawable.emoji_1f202); sSoftbanksMap.append(0xe229, R.drawable.emoji_1f194); sSoftbanksMap.append(0xe22a, R.drawable.emoji_1f235); sSoftbanksMap.append(0xe22b, R.drawable.emoji_1f233); sSoftbanksMap.append(0xe22c, R.drawable.emoji_1f22f); sSoftbanksMap.append(0xe22d, R.drawable.emoji_1f23a); sSoftbanksMap.append(0xe22e, R.drawable.emoji_1f446); sSoftbanksMap.append(0xe22f, R.drawable.emoji_1f447); sSoftbanksMap.append(0xe230, R.drawable.emoji_1f448); sSoftbanksMap.append(0xe231, R.drawable.emoji_1f449); sSoftbanksMap.append(0xe232, R.drawable.emoji_2b06); sSoftbanksMap.append(0xe233, R.drawable.emoji_2b07); sSoftbanksMap.append(0xe234, R.drawable.emoji_27a1); sSoftbanksMap.append(0xe235, R.drawable.emoji_1f519); sSoftbanksMap.append(0xe236, R.drawable.emoji_2197); sSoftbanksMap.append(0xe237, R.drawable.emoji_2196); sSoftbanksMap.append(0xe238, R.drawable.emoji_2198); sSoftbanksMap.append(0xe239, R.drawable.emoji_2199); sSoftbanksMap.append(0xe23a, R.drawable.emoji_25b6); sSoftbanksMap.append(0xe23b, R.drawable.emoji_25c0); sSoftbanksMap.append(0xe23c, R.drawable.emoji_23e9); sSoftbanksMap.append(0xe23d, R.drawable.emoji_23ea); sSoftbanksMap.append(0xe23e, R.drawable.emoji_1f52e); sSoftbanksMap.append(0xe23f, R.drawable.emoji_2648); sSoftbanksMap.append(0xe240, R.drawable.emoji_2649); sSoftbanksMap.append(0xe241, R.drawable.emoji_264a); sSoftbanksMap.append(0xe242, R.drawable.emoji_264b); sSoftbanksMap.append(0xe243, R.drawable.emoji_264c); sSoftbanksMap.append(0xe244, R.drawable.emoji_264d); sSoftbanksMap.append(0xe245, R.drawable.emoji_264e); sSoftbanksMap.append(0xe246, R.drawable.emoji_264f); sSoftbanksMap.append(0xe247, R.drawable.emoji_2650); sSoftbanksMap.append(0xe248, R.drawable.emoji_2651); sSoftbanksMap.append(0xe249, R.drawable.emoji_2652); sSoftbanksMap.append(0xe24a, R.drawable.emoji_2653); sSoftbanksMap.append(0xe24b, R.drawable.emoji_26ce); sSoftbanksMap.append(0xe24c, R.drawable.emoji_1f51d); sSoftbanksMap.append(0xe24d, R.drawable.emoji_1f197); sSoftbanksMap.append(0xe24e, R.drawable.emoji_00a9); sSoftbanksMap.append(0xe24f, R.drawable.emoji_00ae); sSoftbanksMap.append(0xe250, R.drawable.emoji_1f4f3); sSoftbanksMap.append(0xe251, R.drawable.emoji_1f4f4); sSoftbanksMap.append(0xe252, R.drawable.emoji_26a0); sSoftbanksMap.append(0xe253, R.drawable.emoji_1f481); sSoftbanksMap.append(0xe301, R.drawable.emoji_1f4c3); sSoftbanksMap.append(0xe302, R.drawable.emoji_1f454); sSoftbanksMap.append(0xe303, R.drawable.emoji_1f33a); sSoftbanksMap.append(0xe304, R.drawable.emoji_1f337); sSoftbanksMap.append(0xe305, R.drawable.emoji_1f33b); sSoftbanksMap.append(0xe306, R.drawable.emoji_1f490); sSoftbanksMap.append(0xe307, R.drawable.emoji_1f334); sSoftbanksMap.append(0xe308, R.drawable.emoji_1f335); sSoftbanksMap.append(0xe309, R.drawable.emoji_1f6be); sSoftbanksMap.append(0xe30a, R.drawable.emoji_1f3a7); sSoftbanksMap.append(0xe30b, R.drawable.emoji_1f376); sSoftbanksMap.append(0xe30c, R.drawable.emoji_1f37b); sSoftbanksMap.append(0xe30d, R.drawable.emoji_3297); sSoftbanksMap.append(0xe30e, R.drawable.emoji_1f6ac); sSoftbanksMap.append(0xe30f, R.drawable.emoji_1f48a); sSoftbanksMap.append(0xe310, R.drawable.emoji_1f388); sSoftbanksMap.append(0xe311, R.drawable.emoji_1f4a3); sSoftbanksMap.append(0xe312, R.drawable.emoji_1f389); sSoftbanksMap.append(0xe313, R.drawable.emoji_2702); sSoftbanksMap.append(0xe314, R.drawable.emoji_1f380); sSoftbanksMap.append(0xe315, R.drawable.emoji_3299); sSoftbanksMap.append(0xe316, R.drawable.emoji_1f4bd); sSoftbanksMap.append(0xe317, R.drawable.emoji_1f4e3); sSoftbanksMap.append(0xe318, R.drawable.emoji_1f452); sSoftbanksMap.append(0xe319, R.drawable.emoji_1f457); sSoftbanksMap.append(0xe31a, R.drawable.emoji_1f461); sSoftbanksMap.append(0xe31b, R.drawable.emoji_1f462); sSoftbanksMap.append(0xe31c, R.drawable.emoji_1f484); sSoftbanksMap.append(0xe31d, R.drawable.emoji_1f485); sSoftbanksMap.append(0xe31e, R.drawable.emoji_1f486); sSoftbanksMap.append(0xe31f, R.drawable.emoji_1f487); sSoftbanksMap.append(0xe320, R.drawable.emoji_1f488); sSoftbanksMap.append(0xe321, R.drawable.emoji_1f458); sSoftbanksMap.append(0xe322, R.drawable.emoji_1f459); sSoftbanksMap.append(0xe323, R.drawable.emoji_1f45c); sSoftbanksMap.append(0xe324, R.drawable.emoji_1f3ac); sSoftbanksMap.append(0xe325, R.drawable.emoji_1f514); sSoftbanksMap.append(0xe326, R.drawable.emoji_1f3b6); sSoftbanksMap.append(0xe327, R.drawable.emoji_1f493); sSoftbanksMap.append(0xe328, R.drawable.emoji_1f48c); sSoftbanksMap.append(0xe329, R.drawable.emoji_1f498); sSoftbanksMap.append(0xe32a, R.drawable.emoji_1f499); sSoftbanksMap.append(0xe32b, R.drawable.emoji_1f49a); sSoftbanksMap.append(0xe32c, R.drawable.emoji_1f49b); sSoftbanksMap.append(0xe32d, R.drawable.emoji_1f49c); sSoftbanksMap.append(0xe32e, R.drawable.emoji_2728); sSoftbanksMap.append(0xe32f, R.drawable.emoji_2b50); sSoftbanksMap.append(0xe330, R.drawable.emoji_1f4a8); sSoftbanksMap.append(0xe331, R.drawable.emoji_1f4a6); sSoftbanksMap.append(0xe332, R.drawable.emoji_2b55); sSoftbanksMap.append(0xe333, R.drawable.emoji_2716); sSoftbanksMap.append(0xe334, R.drawable.emoji_1f4a2); sSoftbanksMap.append(0xe335, R.drawable.emoji_1f31f); sSoftbanksMap.append(0xe336, R.drawable.emoji_2754); sSoftbanksMap.append(0xe337, R.drawable.emoji_2755); sSoftbanksMap.append(0xe338, R.drawable.emoji_1f375); sSoftbanksMap.append(0xe339, R.drawable.emoji_1f35e); sSoftbanksMap.append(0xe33a, R.drawable.emoji_1f366); sSoftbanksMap.append(0xe33b, R.drawable.emoji_1f35f); sSoftbanksMap.append(0xe33c, R.drawable.emoji_1f361); sSoftbanksMap.append(0xe33d, R.drawable.emoji_1f358); sSoftbanksMap.append(0xe33e, R.drawable.emoji_1f35a); sSoftbanksMap.append(0xe33f, R.drawable.emoji_1f35d); sSoftbanksMap.append(0xe340, R.drawable.emoji_1f35c); sSoftbanksMap.append(0xe341, R.drawable.emoji_1f35b); sSoftbanksMap.append(0xe342, R.drawable.emoji_1f359); sSoftbanksMap.append(0xe343, R.drawable.emoji_1f362); sSoftbanksMap.append(0xe344, R.drawable.emoji_1f363); sSoftbanksMap.append(0xe345, R.drawable.emoji_1f34e); sSoftbanksMap.append(0xe346, R.drawable.emoji_1f34a); sSoftbanksMap.append(0xe347, R.drawable.emoji_1f353); sSoftbanksMap.append(0xe348, R.drawable.emoji_1f349); sSoftbanksMap.append(0xe349, R.drawable.emoji_1f345); sSoftbanksMap.append(0xe34a, R.drawable.emoji_1f346); sSoftbanksMap.append(0xe34b, R.drawable.emoji_1f382); sSoftbanksMap.append(0xe34c, R.drawable.emoji_1f371); sSoftbanksMap.append(0xe34d, R.drawable.emoji_1f372); sSoftbanksMap.append(0xe401, R.drawable.emoji_1f625); sSoftbanksMap.append(0xe402, R.drawable.emoji_1f60f); sSoftbanksMap.append(0xe403, R.drawable.emoji_1f614); sSoftbanksMap.append(0xe404, R.drawable.emoji_1f601); sSoftbanksMap.append(0xe405, R.drawable.emoji_1f609); sSoftbanksMap.append(0xe406, R.drawable.emoji_1f623); sSoftbanksMap.append(0xe407, R.drawable.emoji_1f616); sSoftbanksMap.append(0xe408, R.drawable.emoji_1f62a); sSoftbanksMap.append(0xe409, R.drawable.emoji_1f445); sSoftbanksMap.append(0xe40a, R.drawable.emoji_1f606); sSoftbanksMap.append(0xe40b, R.drawable.emoji_1f628); sSoftbanksMap.append(0xe40c, R.drawable.emoji_1f637); sSoftbanksMap.append(0xe40d, R.drawable.emoji_1f633); sSoftbanksMap.append(0xe40e, R.drawable.emoji_1f612); sSoftbanksMap.append(0xe40f, R.drawable.emoji_1f630); sSoftbanksMap.append(0xe410, R.drawable.emoji_1f632); sSoftbanksMap.append(0xe411, R.drawable.emoji_1f62d); sSoftbanksMap.append(0xe412, R.drawable.emoji_1f602); sSoftbanksMap.append(0xe413, R.drawable.emoji_1f622); sSoftbanksMap.append(0xe414, R.drawable.emoji_263a); sSoftbanksMap.append(0xe415, R.drawable.emoji_1f605); sSoftbanksMap.append(0xe416, R.drawable.emoji_1f621); sSoftbanksMap.append(0xe417, R.drawable.emoji_1f61a); sSoftbanksMap.append(0xe418, R.drawable.emoji_1f618); sSoftbanksMap.append(0xe419, R.drawable.emoji_1f440); sSoftbanksMap.append(0xe41a, R.drawable.emoji_1f443); sSoftbanksMap.append(0xe41b, R.drawable.emoji_1f442); sSoftbanksMap.append(0xe41c, R.drawable.emoji_1f444); sSoftbanksMap.append(0xe41d, R.drawable.emoji_1f64f); sSoftbanksMap.append(0xe41e, R.drawable.emoji_1f44b); sSoftbanksMap.append(0xe41f, R.drawable.emoji_1f44f); sSoftbanksMap.append(0xe420, R.drawable.emoji_1f44c); sSoftbanksMap.append(0xe421, R.drawable.emoji_1f44e); sSoftbanksMap.append(0xe422, R.drawable.emoji_1f450); sSoftbanksMap.append(0xe423, R.drawable.emoji_1f645); sSoftbanksMap.append(0xe424, R.drawable.emoji_1f646); sSoftbanksMap.append(0xe425, R.drawable.emoji_1f491); sSoftbanksMap.append(0xe426, R.drawable.emoji_1f647); sSoftbanksMap.append(0xe427, R.drawable.emoji_1f64c); sSoftbanksMap.append(0xe428, R.drawable.emoji_1f46b); sSoftbanksMap.append(0xe429, R.drawable.emoji_1f46f); sSoftbanksMap.append(0xe42a, R.drawable.emoji_1f3c0); sSoftbanksMap.append(0xe42b, R.drawable.emoji_1f3c8); sSoftbanksMap.append(0xe42c, R.drawable.emoji_1f3b1); sSoftbanksMap.append(0xe42d, R.drawable.emoji_1f3ca); sSoftbanksMap.append(0xe42e, R.drawable.emoji_1f699); sSoftbanksMap.append(0xe42f, R.drawable.emoji_1f69a); sSoftbanksMap.append(0xe430, R.drawable.emoji_1f692); sSoftbanksMap.append(0xe431, R.drawable.emoji_1f691); sSoftbanksMap.append(0xe432, R.drawable.emoji_1f693); sSoftbanksMap.append(0xe433, R.drawable.emoji_1f3a2); sSoftbanksMap.append(0xe434, R.drawable.emoji_1f687); sSoftbanksMap.append(0xe435, R.drawable.emoji_1f684); sSoftbanksMap.append(0xe436, R.drawable.emoji_1f38d); sSoftbanksMap.append(0xe437, R.drawable.emoji_1f49d); sSoftbanksMap.append(0xe438, R.drawable.emoji_1f38e); sSoftbanksMap.append(0xe439, R.drawable.emoji_1f393); sSoftbanksMap.append(0xe43a, R.drawable.emoji_1f392); sSoftbanksMap.append(0xe43b, R.drawable.emoji_1f38f); sSoftbanksMap.append(0xe43c, R.drawable.emoji_1f302); sSoftbanksMap.append(0xe43d, R.drawable.emoji_1f492); sSoftbanksMap.append(0xe43e, R.drawable.emoji_1f30a); sSoftbanksMap.append(0xe43f, R.drawable.emoji_1f367); sSoftbanksMap.append(0xe440, R.drawable.emoji_1f387); sSoftbanksMap.append(0xe441, R.drawable.emoji_1f41a); sSoftbanksMap.append(0xe442, R.drawable.emoji_1f390); sSoftbanksMap.append(0xe443, R.drawable.emoji_1f300); sSoftbanksMap.append(0xe444, R.drawable.emoji_1f33e); sSoftbanksMap.append(0xe445, R.drawable.emoji_1f383); sSoftbanksMap.append(0xe446, R.drawable.emoji_1f391); sSoftbanksMap.append(0xe447, R.drawable.emoji_1f343); sSoftbanksMap.append(0xe448, R.drawable.emoji_1f385); sSoftbanksMap.append(0xe449, R.drawable.emoji_1f305); sSoftbanksMap.append(0xe44a, R.drawable.emoji_1f307); sSoftbanksMap.append(0xe44b, R.drawable.emoji_1f303); sSoftbanksMap.append(0xe44b, R.drawable.emoji_1f30c); sSoftbanksMap.append(0xe44c, R.drawable.emoji_1f308); sSoftbanksMap.append(0xe501, R.drawable.emoji_1f3e9); sSoftbanksMap.append(0xe502, R.drawable.emoji_1f3a8); sSoftbanksMap.append(0xe503, R.drawable.emoji_1f3a9); sSoftbanksMap.append(0xe504, R.drawable.emoji_1f3ec); sSoftbanksMap.append(0xe505, R.drawable.emoji_1f3ef); sSoftbanksMap.append(0xe506, R.drawable.emoji_1f3f0); sSoftbanksMap.append(0xe507, R.drawable.emoji_1f3a6); sSoftbanksMap.append(0xe508, R.drawable.emoji_1f3ed); sSoftbanksMap.append(0xe509, R.drawable.emoji_1f5fc); sSoftbanksMap.append(0xe50b, R.drawable.emoji_1f1ef_1f1f5); sSoftbanksMap.append(0xe50c, R.drawable.emoji_1f1fa_1f1f8); sSoftbanksMap.append(0xe50d, R.drawable.emoji_1f1eb_1f1f7); sSoftbanksMap.append(0xe50e, R.drawable.emoji_1f1e9_1f1ea); sSoftbanksMap.append(0xe50f, R.drawable.emoji_1f1ee_1f1f9); sSoftbanksMap.append(0xe510, R.drawable.emoji_1f1ec_1f1e7); sSoftbanksMap.append(0xe511, R.drawable.emoji_1f1ea_1f1f8); sSoftbanksMap.append(0xe512, R.drawable.emoji_1f1f7_1f1fa); sSoftbanksMap.append(0xe513, R.drawable.emoji_1f1e8_1f1f3); sSoftbanksMap.append(0xe514, R.drawable.emoji_1f1f0_1f1f7); sSoftbanksMap.append(0xe515, R.drawable.emoji_1f471); sSoftbanksMap.append(0xe516, R.drawable.emoji_1f472); sSoftbanksMap.append(0xe517, R.drawable.emoji_1f473); sSoftbanksMap.append(0xe518, R.drawable.emoji_1f474); sSoftbanksMap.append(0xe519, R.drawable.emoji_1f475); sSoftbanksMap.append(0xe51a, R.drawable.emoji_1f476); sSoftbanksMap.append(0xe51b, R.drawable.emoji_1f477); sSoftbanksMap.append(0xe51c, R.drawable.emoji_1f478); sSoftbanksMap.append(0xe51d, R.drawable.emoji_1f5fd); sSoftbanksMap.append(0xe51e, R.drawable.emoji_1f482); sSoftbanksMap.append(0xe51f, R.drawable.emoji_1f483); sSoftbanksMap.append(0xe520, R.drawable.emoji_1f42c); sSoftbanksMap.append(0xe521, R.drawable.emoji_1f426); sSoftbanksMap.append(0xe522, R.drawable.emoji_1f420); sSoftbanksMap.append(0xe523, R.drawable.emoji_1f423); sSoftbanksMap.append(0xe524, R.drawable.emoji_1f439); sSoftbanksMap.append(0xe525, R.drawable.emoji_1f41b); sSoftbanksMap.append(0xe526, R.drawable.emoji_1f418); sSoftbanksMap.append(0xe527, R.drawable.emoji_1f428); sSoftbanksMap.append(0xe528, R.drawable.emoji_1f412); sSoftbanksMap.append(0xe529, R.drawable.emoji_1f411); sSoftbanksMap.append(0xe52a, R.drawable.emoji_1f43a); sSoftbanksMap.append(0xe52b, R.drawable.emoji_1f42e); sSoftbanksMap.append(0xe52c, R.drawable.emoji_1f430); sSoftbanksMap.append(0xe52d, R.drawable.emoji_1f40d); sSoftbanksMap.append(0xe52e, R.drawable.emoji_1f414); sSoftbanksMap.append(0xe52f, R.drawable.emoji_1f417); sSoftbanksMap.append(0xe530, R.drawable.emoji_1f42b); sSoftbanksMap.append(0xe531, R.drawable.emoji_1f438); sSoftbanksMap.append(0xe532, R.drawable.emoji_1f170); sSoftbanksMap.append(0xe533, R.drawable.emoji_1f171); sSoftbanksMap.append(0xe534, R.drawable.emoji_1f18e); sSoftbanksMap.append(0xe535, R.drawable.emoji_1f17e); sSoftbanksMap.append(0xe536, R.drawable.emoji_1f43e); sSoftbanksMap.append(0xe537, R.drawable.emoji_2122); Log.d("emoji", String.format("init emoji cost: %dms", (System.currentTimeMillis() - start))); } private EmojiconHandler() { } private static boolean isSoftBankEmoji(char c) { return ((c >> 12) == 0xe); } private static int getEmojiResource(int codePoint) { return sEmojisMap.get(codePoint); } private static int getSoftbankEmojiResource(char c) { return sSoftbanksMap.get(c); } /** * @param context Convert emoji characters of the given Spannable to the according emojicon. * @param text * @param emojiSize */ public static void addEmojis(Context context, SpannableStringBuilder text, int emojiSize, int textSize) { addEmojis(context, text, emojiSize, textSize, 0, -1, false); } /** * Convert emoji characters of the given Spannable to the according emojicon. * * @param context * @param text * @param emojiSize * @param index * @param length */ public static void addEmojis(Context context, SpannableStringBuilder text, int emojiSize, int textSize, int index, int length) { addEmojis(context, text, emojiSize, textSize, index, length, false); } /** * Convert emoji characters of the given Spannable to the according emojicon. * * @param context * @param text * @param emojiSize * @param useSystemDefault */ public static void addEmojis(Context context, SpannableStringBuilder text, int emojiSize, int textSize, boolean useSystemDefault) { addEmojis(context, text, emojiSize, textSize, 0, -1, useSystemDefault); } /** * Convert emoji characters of the given Spannable to the according emojicon. * * @param context * @param text * @param emojiSize * @param index * @param length * @param useSystemDefault */ public static SpannableStringBuilder addEmojis(Context context, SpannableStringBuilder text, int emojiSize, int textSize, int index, int length, boolean useSystemDefault) { if (useSystemDefault) { return text; } int textLengthToProcess = calculateLegalTextLength(text, index, length); // remove spans throughout all text EmojiconSpan[] oldSpans = text.getSpans(0, text.length(), EmojiconSpan.class); for (EmojiconSpan oldSpan : oldSpans) { text.removeSpan(oldSpan); } int[] results = new int[3]; String textStr = text.toString(); int processIdx = index; while (processIdx < textLengthToProcess) { boolean isEmoji = findEmoji(textStr, processIdx, textLengthToProcess, results); int skip = results[1]; if (isEmoji) { int icon = results[0]; boolean isQQFace = results[2] > 0; EmojiconSpan span = new EmojiconSpan(context, icon, (int) (emojiSize * EMOJIICON_SCALE), (int) (emojiSize * EMOJIICON_SCALE)); span.setTranslateY(isQQFace ? QQFACE_TRANSLATE_Y : EMOJIICON_TRANSLATE_Y); if (span.getCachedDrawable() == null) { text.replace(processIdx, processIdx + skip, ".."); //重新计算字符串的合法长度 textLengthToProcess = calculateLegalTextLength(text, index, length); } else { text.setSpan(span, processIdx, processIdx + skip, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } processIdx += skip; } return (SpannableStringBuilder) text.subSequence(index, processIdx); } /** * 判断文本位于start的字节是否为emoji。 * * @param text * @param start * @param end * @param result 长度为3的数据。当第一位表示emoji的资源id, * 第二位表示emoji在原文本占位长度, * 第三位表示emoji类型是否位qq表情。 * @return 如果是emoji,返回True。 */ public static boolean findEmoji(String text, int start, int end, int[] result) { int skip = 0; int icon = 0; char c = text.charAt(start); if (isSoftBankEmoji(c)) { icon = getSoftbankEmojiResource(c); skip = icon == 0 ? 0 : 1; } if (icon == 0) { int unicode = Character.codePointAt(text, start); skip = Character.charCount(unicode); if (unicode > 0xff) { icon = getEmojiResource(unicode); } if (icon == 0 && start + skip < end) { int followUnicode = Character.codePointAt(text, start + skip); if (followUnicode == 0x20e3) { int followSkip = Character.charCount(followUnicode); switch (unicode) { case 0x0031: icon = R.drawable.emoji_0031; break; case 0x0032: icon = R.drawable.emoji_0032; break; case 0x0033: icon = R.drawable.emoji_0033; break; case 0x0034: icon = R.drawable.emoji_0034; break; case 0x0035: icon = R.drawable.emoji_0035; break; case 0x0036: icon = R.drawable.emoji_0036; break; case 0x0037: icon = R.drawable.emoji_0037; break; case 0x0038: icon = R.drawable.emoji_0038; break; case 0x0039: icon = R.drawable.emoji_0039; break; case 0x0030: icon = R.drawable.emoji_0030; break; case 0x0023: icon = R.drawable.emoji_0023; break; default: followSkip = 0; break; } skip += followSkip; } else { int followSkip = Character.charCount(followUnicode); switch (unicode) { case 0x1f1ef: icon = (followUnicode == 0x1f1f5) ? R.drawable.emoji_1f1ef_1f1f5 : 0; break; case 0x1f1fa: icon = (followUnicode == 0x1f1f8) ? R.drawable.emoji_1f1fa_1f1f8 : 0; break; case 0x1f1eb: icon = (followUnicode == 0x1f1f7) ? R.drawable.emoji_1f1eb_1f1f7 : 0; break; case 0x1f1e9: icon = (followUnicode == 0x1f1ea) ? R.drawable.emoji_1f1e9_1f1ea : 0; break; case 0x1f1ee: icon = (followUnicode == 0x1f1f9) ? R.drawable.emoji_1f1ee_1f1f9 : 0; break; case 0x1f1ec: icon = (followUnicode == 0x1f1e7) ? R.drawable.emoji_1f1ec_1f1e7 : 0; break; case 0x1f1ea: icon = (followUnicode == 0x1f1f8) ? R.drawable.emoji_1f1ea_1f1f8 : 0; break; case 0x1f1f7: icon = (followUnicode == 0x1f1fa) ? R.drawable.emoji_1f1f7_1f1fa : 0; break; case 0x1f1e8: icon = (followUnicode == 0x1f1f3) ? R.drawable.emoji_1f1e8_1f1f3 : 0; break; case 0x1f1f0: icon = (followUnicode == 0x1f1f7) ? R.drawable.emoji_1f1f0_1f1f7 : 0; break; default: followSkip = 0; break; } skip += followSkip; } } } boolean isQQFace = false; if (icon == 0) { if (c == '[') { int emojiCloseIndex = text.indexOf(']', start); if (emojiCloseIndex > 0 && emojiCloseIndex - start <= 4) { CharSequence charSequence = text.subSequence(start, emojiCloseIndex + 1); Integer value = sQQFaceMap.get(charSequence.toString()); if (value != null) { icon = value; skip = emojiCloseIndex + 1 - start; isQQFace = true; } } } } result[0] = icon; result[1] = skip; result[2] = isQQFace ? 1 : 0; return icon > 0; } public static String findQQFaceFileName(String key) { return mQQFaceFileNameList.get(key); } private static int calculateLegalTextLength(SpannableStringBuilder text, int index, int length) { int textLength = text.length(); int textLengthToProcessMax = textLength - index; return (length < 0 || length >= textLengthToProcessMax ? textLength : (length + index)); } public static List getQQFaceKeyList() { return mQQFaceList; } public static boolean isQQFaceCodeExist(String qqFaceCode) { return sQQFaceMap.get(qqFaceCode) != null; } public static class QQFace { private String name; private int res; public QQFace(String name, int res) { this.name = name; this.res = res; } public String getName() { return name; } public int getRes() { return res; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/EmojiconSpan.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.fragment.components.qqface.emojicon; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.style.DynamicDrawableSpan; import java.lang.ref.WeakReference; class EmojiconSpan extends DynamicDrawableSpan { private final Context mContext; private final int mResourceId; private final int mSize; private final int mTextSize; private int mHeight; private int mWidth; private int mTop; private Drawable mDrawable; private WeakReference mDrawableRef; // 手动偏移值 private int mTranslateY = 0; public EmojiconSpan(Context context, int resourceId, int size, int textSize) { super(DynamicDrawableSpan.ALIGN_BASELINE); mContext = context; mResourceId = resourceId; mWidth = mHeight = mSize = size; mTextSize = textSize; } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { // return super.getSize(paint, text, start, end, fm); // fm 以 Paint 的 fm 为基准,避免 Span 改变了 fm 导致文字行高变化 -- chant Drawable d = getCachedDrawable(); Rect rect = d.getBounds(); if (fm != null) { Paint.FontMetricsInt pfm = paint.getFontMetricsInt(); // keep it the same as paint's fm fm.ascent = pfm.ascent; fm.descent = pfm.descent; fm.top = pfm.top; fm.bottom = pfm.bottom; } return rect.right; } public Drawable getDrawable() { if (mDrawable == null) { try { mDrawable = EmojiCache.getInstance().getDrawable(mContext, mResourceId); if (mDrawable != null) { mHeight = mSize; mWidth = mHeight * mDrawable.getIntrinsicWidth() / mDrawable.getIntrinsicHeight(); mTop = (mTextSize - mHeight) / 2; mDrawable.setBounds(0, mTop, mWidth, mTop + mHeight); } } catch (Exception e) { // swallow } } return mDrawable; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { //super.draw(canvas, text, start, end, x, top, y, bottom, paint); Drawable b = getCachedDrawable(); int count = canvas.save(); // int transY = fontMetricesBottom - b.getBounds().bottom; // int transY -= paint.getFontMetricsInt().descent; // 因为 TextView 加了 lineSpacing 之后会导致这里的 bottom、top 参数与单行情况不一样,所以不用 bottom、top,而使用 fontMetrics 的高度来计算 int fontMetricesTop = y + paint.getFontMetricsInt().top; int fontMetricesBottom = fontMetricesTop + (paint.getFontMetricsInt().bottom - paint.getFontMetricsInt().top); int transY = fontMetricesTop + ((fontMetricesBottom - fontMetricesTop) / 2) - ((b.getBounds().bottom - b.getBounds().top) / 2) - mTop; transY += mTranslateY; canvas.translate(x, transY); b.draw(canvas); canvas.restoreToCount(count); } public Drawable getCachedDrawable() { if (mDrawableRef == null || mDrawableRef.get() == null) { mDrawableRef = new WeakReference<>(getDrawable()); } return mDrawableRef.get(); } public void setTranslateY(int translateY) { mTranslateY = translateY; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/EmojiconTextView.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.fragment.components.qqface.emojicon; import android.content.Context; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.AttributeSet; import com.qmuiteam.qmui.widget.textview.QMUISpanTouchFixTextView; public class EmojiconTextView extends QMUISpanTouchFixTextView { private int mEmojiconSize; private int mEmojiconTextSize; private int mTextStart = 0; private int mTextLength = -1; private boolean mUseSystemDefault = false; public EmojiconTextView(Context context) { super(context); init(null); } public EmojiconTextView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public EmojiconTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } private void init(AttributeSet attrs) { mEmojiconTextSize = (int) getTextSize(); mEmojiconSize = (int) getTextSize(); setText(getText()); } public void setTextWithWidth(CharSequence text, int limitedWidth) { if (TextUtils.isEmpty(text)) { super.setText(text); return; } if (limitedWidth < 0) { limitedWidth = this.getMeasuredWidth() - getPaddingRight() - getPaddingLeft(); } SpannableStringBuilder builder = new SpannableStringBuilder(text); EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize, mEmojiconTextSize, mTextStart, mTextLength, mUseSystemDefault); CharSequence trucatedText = TextUtils.ellipsize(builder, getPaint(), limitedWidth, getEllipsize()); super.setText(trucatedText, BufferType.SPANNABLE); } @Override public void setText(CharSequence text, BufferType type) { if (!TextUtils.isEmpty(text)) { SpannableStringBuilder builder = new SpannableStringBuilder(text); EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize, mEmojiconTextSize, mTextStart, mTextLength, mUseSystemDefault); text = builder; } super.setText(text, type); } /** * Set the size of emojicon in pixels. */ public void setEmojiconSize(int pixels) { mEmojiconSize = pixels; super.setText(getText()); } /** * Set whether to use system default emojicon */ public void setUseSystemDefault(boolean useSystemDefault) { mUseSystemDefault = useSystemDefault; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/Emojicon.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.fragment.components.qqface.emojicon.emoji; import java.io.Serializable; public class Emojicon implements Serializable { private static final long serialVersionUID = 1L; private int icon; private char value; private String emoji; private Emojicon() { } public static Emojicon fromResource(int icon, int value) { Emojicon emoji = new Emojicon(); emoji.icon = icon; emoji.value = (char) value; return emoji; } public static Emojicon fromCodePoint(int codePoint) { Emojicon emoji = new Emojicon(); emoji.emoji = newString(codePoint); return emoji; } public static Emojicon fromChar(char ch) { Emojicon emoji = new Emojicon(); emoji.emoji = Character.toString(ch); return emoji; } public static Emojicon fromChars(String chars) { Emojicon emoji = new Emojicon(); emoji.emoji = chars; return emoji; } public Emojicon(String emoji) { this.emoji = emoji; } public char getValue() { return value; } public int getIcon() { return icon; } public String getEmoji() { return emoji; } @Override public boolean equals(Object o) { return o instanceof Emojicon && emoji.equals(((Emojicon) o).emoji); } @Override public int hashCode() { return emoji.hashCode(); } public static String newString(int codePoint) { // Character.charCount 指定字符是否是等于或大于0x10000的,那么该方法返回2。否则,该方法返回1。 if (Character.charCount(codePoint) == 1) { return String.valueOf(codePoint); } else { //指定字符(Unicode代码点)转换成UTF-16表示存储在一个char数组。 return new String(Character.toChars(codePoint)); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/Nature.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.fragment.components.qqface.emojicon.emoji; public class Nature { public static final Emojicon[] DATA = new Emojicon[]{ Emojicon.fromCodePoint(0x1f436), Emojicon.fromCodePoint(0x1f43a), Emojicon.fromCodePoint(0x1f431), Emojicon.fromCodePoint(0x1f42d), Emojicon.fromCodePoint(0x1f439), Emojicon.fromCodePoint(0x1f430), Emojicon.fromCodePoint(0x1f438), Emojicon.fromCodePoint(0x1f42f), Emojicon.fromCodePoint(0x1f428), Emojicon.fromCodePoint(0x1f43b), Emojicon.fromCodePoint(0x1f437), Emojicon.fromCodePoint(0x1f43d), Emojicon.fromCodePoint(0x1f42e), Emojicon.fromCodePoint(0x1f417), Emojicon.fromCodePoint(0x1f435), Emojicon.fromCodePoint(0x1f412), Emojicon.fromCodePoint(0x1f434), Emojicon.fromCodePoint(0x1f411), Emojicon.fromCodePoint(0x1f418), Emojicon.fromCodePoint(0x1f43c), Emojicon.fromCodePoint(0x1f427), Emojicon.fromCodePoint(0x1f426), Emojicon.fromCodePoint(0x1f424), Emojicon.fromCodePoint(0x1f425), Emojicon.fromCodePoint(0x1f423), Emojicon.fromCodePoint(0x1f414), Emojicon.fromCodePoint(0x1f40d), Emojicon.fromCodePoint(0x1f422), Emojicon.fromCodePoint(0x1f41b), Emojicon.fromCodePoint(0x1f41d), Emojicon.fromCodePoint(0x1f41c), Emojicon.fromCodePoint(0x1f41e), Emojicon.fromCodePoint(0x1f40c), Emojicon.fromCodePoint(0x1f419), Emojicon.fromCodePoint(0x1f41a), Emojicon.fromCodePoint(0x1f420), Emojicon.fromCodePoint(0x1f41f), Emojicon.fromCodePoint(0x1f42c), Emojicon.fromCodePoint(0x1f433), Emojicon.fromCodePoint(0x1f40b), Emojicon.fromCodePoint(0x1f404), Emojicon.fromCodePoint(0x1f40f), Emojicon.fromCodePoint(0x1f400), Emojicon.fromCodePoint(0x1f403), Emojicon.fromCodePoint(0x1f405), Emojicon.fromCodePoint(0x1f407), Emojicon.fromCodePoint(0x1f409), Emojicon.fromCodePoint(0x1f40e), Emojicon.fromCodePoint(0x1f410), Emojicon.fromCodePoint(0x1f413), Emojicon.fromCodePoint(0x1f415), Emojicon.fromCodePoint(0x1f416), Emojicon.fromCodePoint(0x1f401), Emojicon.fromCodePoint(0x1f402), Emojicon.fromCodePoint(0x1f432), Emojicon.fromCodePoint(0x1f421), Emojicon.fromCodePoint(0x1f40a), Emojicon.fromCodePoint(0x1f42b), Emojicon.fromCodePoint(0x1f42a), Emojicon.fromCodePoint(0x1f406), Emojicon.fromCodePoint(0x1f408), Emojicon.fromCodePoint(0x1f429), Emojicon.fromCodePoint(0x1f43e), Emojicon.fromCodePoint(0x1f490), Emojicon.fromCodePoint(0x1f338), Emojicon.fromCodePoint(0x1f337), Emojicon.fromCodePoint(0x1f340), Emojicon.fromCodePoint(0x1f339), Emojicon.fromCodePoint(0x1f33b), Emojicon.fromCodePoint(0x1f33a), Emojicon.fromCodePoint(0x1f341), Emojicon.fromCodePoint(0x1f343), Emojicon.fromCodePoint(0x1f342), Emojicon.fromCodePoint(0x1f33f), Emojicon.fromCodePoint(0x1f33e), Emojicon.fromCodePoint(0x1f344), Emojicon.fromCodePoint(0x1f335), Emojicon.fromCodePoint(0x1f334), Emojicon.fromCodePoint(0x1f332), Emojicon.fromCodePoint(0x1f333), Emojicon.fromCodePoint(0x1f330), Emojicon.fromCodePoint(0x1f331), Emojicon.fromCodePoint(0x1f33c), Emojicon.fromCodePoint(0x1f310), Emojicon.fromCodePoint(0x1f31e), Emojicon.fromCodePoint(0x1f31d), Emojicon.fromCodePoint(0x1f31a), Emojicon.fromCodePoint(0x1f311), Emojicon.fromCodePoint(0x1f312), Emojicon.fromCodePoint(0x1f313), Emojicon.fromCodePoint(0x1f314), Emojicon.fromCodePoint(0x1f315), Emojicon.fromCodePoint(0x1f316), Emojicon.fromCodePoint(0x1f317), Emojicon.fromCodePoint(0x1f318), Emojicon.fromCodePoint(0x1f31c), Emojicon.fromCodePoint(0x1f31b), Emojicon.fromCodePoint(0x1f319), Emojicon.fromCodePoint(0x1f30d), Emojicon.fromCodePoint(0x1f30e), Emojicon.fromCodePoint(0x1f30f), Emojicon.fromCodePoint(0x1f30b), Emojicon.fromCodePoint(0x1f30c), Emojicon.fromCodePoint(0x1f320), Emojicon.fromChar((char) 0x2b50), Emojicon.fromChar((char) 0x2600), Emojicon.fromChar((char) 0x26c5), Emojicon.fromChar((char) 0x2601), Emojicon.fromChar((char) 0x26a1), Emojicon.fromChar((char) 0x2614), Emojicon.fromChar((char) 0x2744), Emojicon.fromChar((char) 0x26c4), Emojicon.fromCodePoint(0x1f300), Emojicon.fromCodePoint(0x1f301), Emojicon.fromCodePoint(0x1f308), Emojicon.fromCodePoint(0x1f30a), }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/Objects.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.fragment.components.qqface.emojicon.emoji; public class Objects { public static final Emojicon[] DATA = new Emojicon[]{ Emojicon.fromCodePoint(0x1f38d), Emojicon.fromCodePoint(0x1f49d), Emojicon.fromCodePoint(0x1f38e), Emojicon.fromCodePoint(0x1f392), Emojicon.fromCodePoint(0x1f393), Emojicon.fromCodePoint(0x1f38f), Emojicon.fromCodePoint(0x1f386), Emojicon.fromCodePoint(0x1f387), Emojicon.fromCodePoint(0x1f390), Emojicon.fromCodePoint(0x1f391), Emojicon.fromCodePoint(0x1f383), Emojicon.fromCodePoint(0x1f47b), Emojicon.fromCodePoint(0x1f385), Emojicon.fromCodePoint(0x1f384), Emojicon.fromCodePoint(0x1f381), Emojicon.fromCodePoint(0x1f38b), Emojicon.fromCodePoint(0x1f389), Emojicon.fromCodePoint(0x1f38a), Emojicon.fromCodePoint(0x1f388), Emojicon.fromCodePoint(0x1f38c), Emojicon.fromCodePoint(0x1f52e), Emojicon.fromCodePoint(0x1f3a5), Emojicon.fromCodePoint(0x1f4f7), Emojicon.fromCodePoint(0x1f4f9), Emojicon.fromCodePoint(0x1f4fc), Emojicon.fromCodePoint(0x1f4bf), Emojicon.fromCodePoint(0x1f4c0), Emojicon.fromCodePoint(0x1f4bd), Emojicon.fromCodePoint(0x1f4be), Emojicon.fromCodePoint(0x1f4bb), Emojicon.fromCodePoint(0x1f4f1), Emojicon.fromChar((char) 0x260e), Emojicon.fromCodePoint(0x1f4de), Emojicon.fromCodePoint(0x1f4df), Emojicon.fromCodePoint(0x1f4e0), Emojicon.fromCodePoint(0x1f4e1), Emojicon.fromCodePoint(0x1f4fa), Emojicon.fromCodePoint(0x1f4fb), Emojicon.fromCodePoint(0x1f508), Emojicon.fromCodePoint(0x1f509), Emojicon.fromCodePoint(0x1f50a), Emojicon.fromCodePoint(0x1f507), Emojicon.fromCodePoint(0x1f514), Emojicon.fromCodePoint(0x1f515), Emojicon.fromCodePoint(0x1f4e2), Emojicon.fromCodePoint(0x1f4e3), Emojicon.fromChar((char) 0x23f3), Emojicon.fromChar((char) 0x231b), Emojicon.fromChar((char) 0x23f0), Emojicon.fromChar((char) 0x231a), Emojicon.fromCodePoint(0x1f513), Emojicon.fromCodePoint(0x1f512), Emojicon.fromCodePoint(0x1f50f), Emojicon.fromCodePoint(0x1f510), Emojicon.fromCodePoint(0x1f511), Emojicon.fromCodePoint(0x1f50e), Emojicon.fromCodePoint(0x1f4a1), Emojicon.fromCodePoint(0x1f526), Emojicon.fromCodePoint(0x1f506), Emojicon.fromCodePoint(0x1f505), Emojicon.fromCodePoint(0x1f50c), Emojicon.fromCodePoint(0x1f50b), Emojicon.fromCodePoint(0x1f50d), Emojicon.fromCodePoint(0x1f6c1), Emojicon.fromCodePoint(0x1f6c0), Emojicon.fromCodePoint(0x1f6bf), Emojicon.fromCodePoint(0x1f6bd), Emojicon.fromCodePoint(0x1f527), Emojicon.fromCodePoint(0x1f529), Emojicon.fromCodePoint(0x1f528), Emojicon.fromCodePoint(0x1f6aa), Emojicon.fromCodePoint(0x1f6ac), Emojicon.fromCodePoint(0x1f4a3), Emojicon.fromCodePoint(0x1f52b), Emojicon.fromCodePoint(0x1f52a), Emojicon.fromCodePoint(0x1f48a), Emojicon.fromCodePoint(0x1f489), Emojicon.fromCodePoint(0x1f4b0), Emojicon.fromCodePoint(0x1f4b4), Emojicon.fromCodePoint(0x1f4b5), Emojicon.fromCodePoint(0x1f4b7), Emojicon.fromCodePoint(0x1f4b6), Emojicon.fromCodePoint(0x1f4b3), Emojicon.fromCodePoint(0x1f4b8), Emojicon.fromCodePoint(0x1f4f2), Emojicon.fromCodePoint(0x1f4e7), Emojicon.fromCodePoint(0x1f4e5), Emojicon.fromCodePoint(0x1f4e4), Emojicon.fromChar((char) 0x2709), Emojicon.fromCodePoint(0x1f4e9), Emojicon.fromCodePoint(0x1f4e8), Emojicon.fromCodePoint(0x1f4ef), Emojicon.fromCodePoint(0x1f4eb), Emojicon.fromCodePoint(0x1f4ea), Emojicon.fromCodePoint(0x1f4ec), Emojicon.fromCodePoint(0x1f4ed), Emojicon.fromCodePoint(0x1f4ee), Emojicon.fromCodePoint(0x1f4e6), Emojicon.fromCodePoint(0x1f4dd), Emojicon.fromCodePoint(0x1f4c4), Emojicon.fromCodePoint(0x1f4c3), Emojicon.fromCodePoint(0x1f4d1), Emojicon.fromCodePoint(0x1f4ca), Emojicon.fromCodePoint(0x1f4c8), Emojicon.fromCodePoint(0x1f4c9), Emojicon.fromCodePoint(0x1f4dc), Emojicon.fromCodePoint(0x1f4cb), Emojicon.fromCodePoint(0x1f4c5), Emojicon.fromCodePoint(0x1f4c6), Emojicon.fromCodePoint(0x1f4c7), Emojicon.fromCodePoint(0x1f4c1), Emojicon.fromCodePoint(0x1f4c2), Emojicon.fromChar((char) 0x2702), Emojicon.fromCodePoint(0x1f4cc), Emojicon.fromCodePoint(0x1f4ce), Emojicon.fromChar((char) 0x2712), Emojicon.fromChar((char) 0x270f), Emojicon.fromCodePoint(0x1f4cf), Emojicon.fromCodePoint(0x1f4d0), Emojicon.fromCodePoint(0x1f4d5), Emojicon.fromCodePoint(0x1f4d7), Emojicon.fromCodePoint(0x1f4d8), Emojicon.fromCodePoint(0x1f4d9), Emojicon.fromCodePoint(0x1f4d3), Emojicon.fromCodePoint(0x1f4d4), Emojicon.fromCodePoint(0x1f4d2), Emojicon.fromCodePoint(0x1f4da), Emojicon.fromCodePoint(0x1f4d6), Emojicon.fromCodePoint(0x1f516), Emojicon.fromCodePoint(0x1f4db), Emojicon.fromCodePoint(0x1f52c), Emojicon.fromCodePoint(0x1f52d), Emojicon.fromCodePoint(0x1f4f0), Emojicon.fromCodePoint(0x1f3a8), Emojicon.fromCodePoint(0x1f3ac), Emojicon.fromCodePoint(0x1f3a4), Emojicon.fromCodePoint(0x1f3a7), Emojicon.fromCodePoint(0x1f3bc), Emojicon.fromCodePoint(0x1f3b5), Emojicon.fromCodePoint(0x1f3b6), Emojicon.fromCodePoint(0x1f3b9), Emojicon.fromCodePoint(0x1f3bb), Emojicon.fromCodePoint(0x1f3ba), Emojicon.fromCodePoint(0x1f3b7), Emojicon.fromCodePoint(0x1f3b8), Emojicon.fromCodePoint(0x1f47e), Emojicon.fromCodePoint(0x1f3ae), Emojicon.fromCodePoint(0x1f0cf), Emojicon.fromCodePoint(0x1f3b4), Emojicon.fromCodePoint(0x1f004), Emojicon.fromCodePoint(0x1f3b2), Emojicon.fromCodePoint(0x1f3af), Emojicon.fromCodePoint(0x1f3c8), Emojicon.fromCodePoint(0x1f3c0), Emojicon.fromChar((char) 0x26bd), Emojicon.fromChar((char) 0x26be), Emojicon.fromCodePoint(0x1f3be), Emojicon.fromCodePoint(0x1f3b1), Emojicon.fromCodePoint(0x1f3c9), Emojicon.fromCodePoint(0x1f3b3), Emojicon.fromChar((char) 0x26f3), Emojicon.fromCodePoint(0x1f6b5), Emojicon.fromCodePoint(0x1f6b4), Emojicon.fromCodePoint(0x1f3c1), Emojicon.fromCodePoint(0x1f3c7), Emojicon.fromCodePoint(0x1f3c6), Emojicon.fromCodePoint(0x1f3bf), Emojicon.fromCodePoint(0x1f3c2), Emojicon.fromCodePoint(0x1f3ca), Emojicon.fromCodePoint(0x1f3c4), Emojicon.fromCodePoint(0x1f3a3), Emojicon.fromChar((char) 0x2615), Emojicon.fromCodePoint(0x1f375), Emojicon.fromCodePoint(0x1f376), Emojicon.fromCodePoint(0x1f37c), Emojicon.fromCodePoint(0x1f37a), Emojicon.fromCodePoint(0x1f37b), Emojicon.fromCodePoint(0x1f378), Emojicon.fromCodePoint(0x1f379), Emojicon.fromCodePoint(0x1f377), Emojicon.fromCodePoint(0x1f374), Emojicon.fromCodePoint(0x1f355), Emojicon.fromCodePoint(0x1f354), Emojicon.fromCodePoint(0x1f35f), Emojicon.fromCodePoint(0x1f357), Emojicon.fromCodePoint(0x1f356), Emojicon.fromCodePoint(0x1f35d), Emojicon.fromCodePoint(0x1f35b), Emojicon.fromCodePoint(0x1f364), Emojicon.fromCodePoint(0x1f371), Emojicon.fromCodePoint(0x1f363), Emojicon.fromCodePoint(0x1f365), Emojicon.fromCodePoint(0x1f359), Emojicon.fromCodePoint(0x1f358), Emojicon.fromCodePoint(0x1f35a), Emojicon.fromCodePoint(0x1f35c), Emojicon.fromCodePoint(0x1f372), Emojicon.fromCodePoint(0x1f362), Emojicon.fromCodePoint(0x1f361), Emojicon.fromCodePoint(0x1f373), Emojicon.fromCodePoint(0x1f35e), Emojicon.fromCodePoint(0x1f369), Emojicon.fromCodePoint(0x1f36e), Emojicon.fromCodePoint(0x1f366), Emojicon.fromCodePoint(0x1f368), Emojicon.fromCodePoint(0x1f367), Emojicon.fromCodePoint(0x1f382), Emojicon.fromCodePoint(0x1f370), Emojicon.fromCodePoint(0x1f36a), Emojicon.fromCodePoint(0x1f36b), Emojicon.fromCodePoint(0x1f36c), Emojicon.fromCodePoint(0x1f36d), Emojicon.fromCodePoint(0x1f36f), Emojicon.fromCodePoint(0x1f34e), Emojicon.fromCodePoint(0x1f34f), Emojicon.fromCodePoint(0x1f34a), Emojicon.fromCodePoint(0x1f34b), Emojicon.fromCodePoint(0x1f352), Emojicon.fromCodePoint(0x1f347), Emojicon.fromCodePoint(0x1f349), Emojicon.fromCodePoint(0x1f353), Emojicon.fromCodePoint(0x1f351), Emojicon.fromCodePoint(0x1f348), Emojicon.fromCodePoint(0x1f34c), Emojicon.fromCodePoint(0x1f350), Emojicon.fromCodePoint(0x1f34d), Emojicon.fromCodePoint(0x1f360), Emojicon.fromCodePoint(0x1f346), Emojicon.fromCodePoint(0x1f345), Emojicon.fromCodePoint(0x1f33d), }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/People.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.fragment.components.qqface.emojicon.emoji; public class People { public static final Emojicon[] DATA = new Emojicon[]{ Emojicon.fromCodePoint(0x1f604), Emojicon.fromCodePoint(0x1f603), Emojicon.fromCodePoint(0x1f600), Emojicon.fromCodePoint(0x1f60a), Emojicon.fromChar((char) 0x263a), Emojicon.fromCodePoint(0x1f609), Emojicon.fromCodePoint(0x1f60d), Emojicon.fromCodePoint(0x1f618), Emojicon.fromCodePoint(0x1f61a), Emojicon.fromCodePoint(0x1f617), Emojicon.fromCodePoint(0x1f619), Emojicon.fromCodePoint(0x1f61c), Emojicon.fromCodePoint(0x1f61d), Emojicon.fromCodePoint(0x1f61b), Emojicon.fromCodePoint(0x1f633), Emojicon.fromCodePoint(0x1f601), Emojicon.fromCodePoint(0x1f614), Emojicon.fromCodePoint(0x1f60c), Emojicon.fromCodePoint(0x1f612), Emojicon.fromCodePoint(0x1f61e), Emojicon.fromCodePoint(0x1f623), Emojicon.fromCodePoint(0x1f622), Emojicon.fromCodePoint(0x1f602), Emojicon.fromCodePoint(0x1f62d), Emojicon.fromCodePoint(0x1f62a), Emojicon.fromCodePoint(0x1f625), Emojicon.fromCodePoint(0x1f630), Emojicon.fromCodePoint(0x1f605), Emojicon.fromCodePoint(0x1f613), Emojicon.fromCodePoint(0x1f629), Emojicon.fromCodePoint(0x1f62b), Emojicon.fromCodePoint(0x1f628), Emojicon.fromCodePoint(0x1f631), Emojicon.fromCodePoint(0x1f620), Emojicon.fromCodePoint(0x1f621), Emojicon.fromCodePoint(0x1f624), Emojicon.fromCodePoint(0x1f616), Emojicon.fromCodePoint(0x1f606), Emojicon.fromCodePoint(0x1f60b), Emojicon.fromCodePoint(0x1f637), Emojicon.fromCodePoint(0x1f60e), Emojicon.fromCodePoint(0x1f634), Emojicon.fromCodePoint(0x1f635), Emojicon.fromCodePoint(0x1f632), Emojicon.fromCodePoint(0x1f61f), Emojicon.fromCodePoint(0x1f626), Emojicon.fromCodePoint(0x1f627), Emojicon.fromCodePoint(0x1f608), Emojicon.fromCodePoint(0x1f47f), Emojicon.fromCodePoint(0x1f62e), Emojicon.fromCodePoint(0x1f62c), Emojicon.fromCodePoint(0x1f610), Emojicon.fromCodePoint(0x1f615), Emojicon.fromCodePoint(0x1f62f), Emojicon.fromCodePoint(0x1f636), Emojicon.fromCodePoint(0x1f607), Emojicon.fromCodePoint(0x1f60f), Emojicon.fromCodePoint(0x1f611), Emojicon.fromCodePoint(0x1f472), Emojicon.fromCodePoint(0x1f473), Emojicon.fromCodePoint(0x1f46e), Emojicon.fromCodePoint(0x1f477), Emojicon.fromCodePoint(0x1f482), Emojicon.fromCodePoint(0x1f476), Emojicon.fromCodePoint(0x1f466), Emojicon.fromCodePoint(0x1f467), Emojicon.fromCodePoint(0x1f468), Emojicon.fromCodePoint(0x1f469), Emojicon.fromCodePoint(0x1f474), Emojicon.fromCodePoint(0x1f475), Emojicon.fromCodePoint(0x1f471), Emojicon.fromCodePoint(0x1f47c), Emojicon.fromCodePoint(0x1f478), Emojicon.fromCodePoint(0x1f63a), Emojicon.fromCodePoint(0x1f638), Emojicon.fromCodePoint(0x1f63b), Emojicon.fromCodePoint(0x1f63d), Emojicon.fromCodePoint(0x1f63c), Emojicon.fromCodePoint(0x1f640), Emojicon.fromCodePoint(0x1f63f), Emojicon.fromCodePoint(0x1f639), Emojicon.fromCodePoint(0x1f63e), Emojicon.fromCodePoint(0x1f479), Emojicon.fromCodePoint(0x1f47a), Emojicon.fromCodePoint(0x1f648), Emojicon.fromCodePoint(0x1f649), Emojicon.fromCodePoint(0x1f64a), Emojicon.fromCodePoint(0x1f480), Emojicon.fromCodePoint(0x1f47d), Emojicon.fromCodePoint(0x1f4a9), Emojicon.fromCodePoint(0x1f525), Emojicon.fromChar((char) 0x2728), Emojicon.fromCodePoint(0x1f31f), Emojicon.fromCodePoint(0x1f4ab), Emojicon.fromCodePoint(0x1f4a5), Emojicon.fromCodePoint(0x1f4a2), Emojicon.fromCodePoint(0x1f4a6), Emojicon.fromCodePoint(0x1f4a7), Emojicon.fromCodePoint(0x1f4a4), Emojicon.fromCodePoint(0x1f4a8), Emojicon.fromCodePoint(0x1f442), Emojicon.fromCodePoint(0x1f440), Emojicon.fromCodePoint(0x1f443), Emojicon.fromCodePoint(0x1f445), Emojicon.fromCodePoint(0x1f444), Emojicon.fromCodePoint(0x1f44d), Emojicon.fromCodePoint(0x1f44e), Emojicon.fromCodePoint(0x1f44c), Emojicon.fromCodePoint(0x1f44a), Emojicon.fromChar((char) 0x270a), Emojicon.fromChar((char) 0x270c), Emojicon.fromCodePoint(0x1f44b), Emojicon.fromChar((char) 0x270b), Emojicon.fromCodePoint(0x1f450), Emojicon.fromCodePoint(0x1f446), Emojicon.fromCodePoint(0x1f447), Emojicon.fromCodePoint(0x1f449), Emojicon.fromCodePoint(0x1f448), Emojicon.fromCodePoint(0x1f64c), Emojicon.fromCodePoint(0x1f64f), Emojicon.fromChar((char) 0x261d), Emojicon.fromCodePoint(0x1f44f), Emojicon.fromCodePoint(0x1f4aa), Emojicon.fromCodePoint(0x1f6b6), Emojicon.fromCodePoint(0x1f3c3), Emojicon.fromCodePoint(0x1f483), Emojicon.fromCodePoint(0x1f46b), Emojicon.fromCodePoint(0x1f46a), Emojicon.fromCodePoint(0x1f46c), Emojicon.fromCodePoint(0x1f46d), Emojicon.fromCodePoint(0x1f48f), Emojicon.fromCodePoint(0x1f491), Emojicon.fromCodePoint(0x1f46f), Emojicon.fromCodePoint(0x1f646), Emojicon.fromCodePoint(0x1f645), Emojicon.fromCodePoint(0x1f481), Emojicon.fromCodePoint(0x1f64b), Emojicon.fromCodePoint(0x1f486), Emojicon.fromCodePoint(0x1f487), Emojicon.fromCodePoint(0x1f485), Emojicon.fromCodePoint(0x1f470), Emojicon.fromCodePoint(0x1f64e), Emojicon.fromCodePoint(0x1f64d), Emojicon.fromCodePoint(0x1f647), Emojicon.fromCodePoint(0x1f3a9), Emojicon.fromCodePoint(0x1f451), Emojicon.fromCodePoint(0x1f452), Emojicon.fromCodePoint(0x1f45f), Emojicon.fromCodePoint(0x1f45e), Emojicon.fromCodePoint(0x1f461), Emojicon.fromCodePoint(0x1f460), Emojicon.fromCodePoint(0x1f462), Emojicon.fromCodePoint(0x1f455), Emojicon.fromCodePoint(0x1f454), Emojicon.fromCodePoint(0x1f45a), Emojicon.fromCodePoint(0x1f457), Emojicon.fromCodePoint(0x1f3bd), Emojicon.fromCodePoint(0x1f456), Emojicon.fromCodePoint(0x1f458), Emojicon.fromCodePoint(0x1f459), Emojicon.fromCodePoint(0x1f4bc), Emojicon.fromCodePoint(0x1f45c), Emojicon.fromCodePoint(0x1f45d), Emojicon.fromCodePoint(0x1f45b), Emojicon.fromCodePoint(0x1f453), Emojicon.fromCodePoint(0x1f380), Emojicon.fromCodePoint(0x1f302), Emojicon.fromCodePoint(0x1f484), Emojicon.fromCodePoint(0x1f49b), Emojicon.fromCodePoint(0x1f499), Emojicon.fromCodePoint(0x1f49c), Emojicon.fromCodePoint(0x1f49a), Emojicon.fromChar((char) 0x2764), Emojicon.fromCodePoint(0x1f494), Emojicon.fromCodePoint(0x1f497), Emojicon.fromCodePoint(0x1f493), Emojicon.fromCodePoint(0x1f495), Emojicon.fromCodePoint(0x1f496), Emojicon.fromCodePoint(0x1f49e), Emojicon.fromCodePoint(0x1f498), Emojicon.fromCodePoint(0x1f48c), Emojicon.fromCodePoint(0x1f48b), Emojicon.fromCodePoint(0x1f48d), Emojicon.fromCodePoint(0x1f48e), Emojicon.fromCodePoint(0x1f464), Emojicon.fromCodePoint(0x1f465), Emojicon.fromCodePoint(0x1f4ac), Emojicon.fromCodePoint(0x1f463), Emojicon.fromCodePoint(0x1f4ad), }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/Places.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.fragment.components.qqface.emojicon.emoji; public class Places { public static final Emojicon[] DATA = new Emojicon[]{ Emojicon.fromCodePoint(0x1f3e0), Emojicon.fromCodePoint(0x1f3e1), Emojicon.fromCodePoint(0x1f3eb), Emojicon.fromCodePoint(0x1f3e2), Emojicon.fromCodePoint(0x1f3e3), Emojicon.fromCodePoint(0x1f3e5), Emojicon.fromCodePoint(0x1f3e6), Emojicon.fromCodePoint(0x1f3ea), Emojicon.fromCodePoint(0x1f3e9), Emojicon.fromCodePoint(0x1f3e8), Emojicon.fromCodePoint(0x1f492), Emojicon.fromChar((char) 0x26ea), Emojicon.fromCodePoint(0x1f3ec), Emojicon.fromCodePoint(0x1f3e4), Emojicon.fromCodePoint(0x1f307), Emojicon.fromCodePoint(0x1f306), Emojicon.fromCodePoint(0x1f3ef), Emojicon.fromCodePoint(0x1f3f0), Emojicon.fromChar((char) 0x26fa), Emojicon.fromCodePoint(0x1f3ed), Emojicon.fromCodePoint(0x1f5fc), Emojicon.fromCodePoint(0x1f5fe), Emojicon.fromCodePoint(0x1f5fb), Emojicon.fromCodePoint(0x1f304), Emojicon.fromCodePoint(0x1f305), Emojicon.fromCodePoint(0x1f303), Emojicon.fromCodePoint(0x1f5fd), Emojicon.fromCodePoint(0x1f309), Emojicon.fromCodePoint(0x1f3a0), Emojicon.fromCodePoint(0x1f3a1), Emojicon.fromChar((char) 0x26f2), Emojicon.fromCodePoint(0x1f3a2), Emojicon.fromCodePoint(0x1f6a2), Emojicon.fromChar((char) 0x26f5), Emojicon.fromCodePoint(0x1f6a4), Emojicon.fromCodePoint(0x1f6a3), Emojicon.fromChar((char) 0x2693), Emojicon.fromCodePoint(0x1f680), Emojicon.fromChar((char) 0x2708), Emojicon.fromCodePoint(0x1f4ba), Emojicon.fromCodePoint(0x1f681), Emojicon.fromCodePoint(0x1f682), Emojicon.fromCodePoint(0x1f68a), Emojicon.fromCodePoint(0x1f689), Emojicon.fromCodePoint(0x1f69e), Emojicon.fromCodePoint(0x1f686), Emojicon.fromCodePoint(0x1f684), Emojicon.fromCodePoint(0x1f685), Emojicon.fromCodePoint(0x1f688), Emojicon.fromCodePoint(0x1f687), Emojicon.fromCodePoint(0x1f69d), Emojicon.fromCodePoint(0x1f68b), Emojicon.fromCodePoint(0x1f683), Emojicon.fromCodePoint(0x1f68e), Emojicon.fromCodePoint(0x1f68c), Emojicon.fromCodePoint(0x1f68d), Emojicon.fromCodePoint(0x1f699), Emojicon.fromCodePoint(0x1f698), Emojicon.fromCodePoint(0x1f697), Emojicon.fromCodePoint(0x1f695), Emojicon.fromCodePoint(0x1f696), Emojicon.fromCodePoint(0x1f69b), Emojicon.fromCodePoint(0x1f69a), Emojicon.fromCodePoint(0x1f6a8), Emojicon.fromCodePoint(0x1f693), Emojicon.fromCodePoint(0x1f694), Emojicon.fromCodePoint(0x1f692), Emojicon.fromCodePoint(0x1f691), Emojicon.fromCodePoint(0x1f690), Emojicon.fromCodePoint(0x1f6b2), Emojicon.fromCodePoint(0x1f6a1), Emojicon.fromCodePoint(0x1f69f), Emojicon.fromCodePoint(0x1f6a0), Emojicon.fromCodePoint(0x1f69c), Emojicon.fromCodePoint(0x1f488), Emojicon.fromCodePoint(0x1f68f), Emojicon.fromCodePoint(0x1f3ab), Emojicon.fromCodePoint(0x1f6a6), Emojicon.fromCodePoint(0x1f6a5), Emojicon.fromChar((char) 0x26a0), Emojicon.fromCodePoint(0x1f6a7), Emojicon.fromCodePoint(0x1f530), Emojicon.fromChar((char) 0x26fd), Emojicon.fromCodePoint(0x1f3ee), Emojicon.fromCodePoint(0x1f3b0), Emojicon.fromChar((char) 0x2668), Emojicon.fromCodePoint(0x1f5ff), Emojicon.fromCodePoint(0x1f3aa), Emojicon.fromCodePoint(0x1f3ad), Emojicon.fromCodePoint(0x1f4cd), Emojicon.fromCodePoint(0x1f6a9), Emojicon.fromChars("\ud83c\uddef\ud83c\uddf5"), Emojicon.fromChars("\ud83c\uddf0\ud83c\uddf7"), Emojicon.fromChars("\ud83c\udde9\ud83c\uddea"), Emojicon.fromChars("\ud83c\udde8\ud83c\uddf3"), Emojicon.fromChars("\ud83c\uddfa\ud83c\uddf8"), Emojicon.fromChars("\ud83c\uddeb\ud83c\uddf7"), Emojicon.fromChars("\ud83c\uddea\ud83c\uddf8"), Emojicon.fromChars("\ud83c\uddee\ud83c\uddf9"), Emojicon.fromChars("\ud83c\uddf7\ud83c\uddfa"), Emojicon.fromChars("\ud83c\uddec\ud83c\udde7"), }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/emojicon/emoji/Symbols.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.fragment.components.qqface.emojicon.emoji; public class Symbols { public static final Emojicon[] DATA = new Emojicon[]{ Emojicon.fromChars("\u0031\u20e3"), Emojicon.fromChars("\u0032\u20e3"), Emojicon.fromChars("\u0033\u20e3"), Emojicon.fromChars("\u0034\u20e3"), Emojicon.fromChars("\u0035\u20e3"), Emojicon.fromChars("\u0036\u20e3"), Emojicon.fromChars("\u0037\u20e3"), Emojicon.fromChars("\u0038\u20e3"), Emojicon.fromChars("\u0039\u20e3"), Emojicon.fromChars("\u0030\u20e3"), Emojicon.fromCodePoint(0x1f51f), Emojicon.fromCodePoint(0x1f522), Emojicon.fromChars("\u0023\u20e3"), Emojicon.fromCodePoint(0x1f523), Emojicon.fromChar((char) 0x2b06), Emojicon.fromChar((char) 0x2b07), Emojicon.fromChar((char) 0x2b05), Emojicon.fromChar((char) 0x27a1), Emojicon.fromCodePoint(0x1f520), Emojicon.fromCodePoint(0x1f521), Emojicon.fromCodePoint(0x1f524), Emojicon.fromChar((char) 0x2197), Emojicon.fromChar((char) 0x2196), Emojicon.fromChar((char) 0x2198), Emojicon.fromChar((char) 0x2199), Emojicon.fromChar((char) 0x2194), Emojicon.fromChar((char) 0x2195), Emojicon.fromCodePoint(0x1f504), Emojicon.fromChar((char) 0x25c0), Emojicon.fromChar((char) 0x25b6), Emojicon.fromCodePoint(0x1f53c), Emojicon.fromCodePoint(0x1f53d), Emojicon.fromChar((char) 0x21a9), Emojicon.fromChar((char) 0x21aa), Emojicon.fromChar((char) 0x2139), Emojicon.fromChar((char) 0x23ea), Emojicon.fromChar((char) 0x23e9), Emojicon.fromChar((char) 0x23eb), Emojicon.fromChar((char) 0x23ec), Emojicon.fromChar((char) 0x2935), Emojicon.fromChar((char) 0x2934), Emojicon.fromCodePoint(0x1f197), Emojicon.fromCodePoint(0x1f500), Emojicon.fromCodePoint(0x1f501), Emojicon.fromCodePoint(0x1f502), Emojicon.fromCodePoint(0x1f195), Emojicon.fromCodePoint(0x1f199), Emojicon.fromCodePoint(0x1f192), Emojicon.fromCodePoint(0x1f193), Emojicon.fromCodePoint(0x1f196), Emojicon.fromCodePoint(0x1f4f6), Emojicon.fromCodePoint(0x1f3a6), Emojicon.fromCodePoint(0x1f201), Emojicon.fromCodePoint(0x1f22f), Emojicon.fromCodePoint(0x1f233), Emojicon.fromCodePoint(0x1f235), Emojicon.fromCodePoint(0x1f234), Emojicon.fromCodePoint(0x1f232), Emojicon.fromCodePoint(0x1f250), Emojicon.fromCodePoint(0x1f239), Emojicon.fromCodePoint(0x1f23a), Emojicon.fromCodePoint(0x1f236), Emojicon.fromCodePoint(0x1f21a), Emojicon.fromCodePoint(0x1f6bb), Emojicon.fromCodePoint(0x1f6b9), Emojicon.fromCodePoint(0x1f6ba), Emojicon.fromCodePoint(0x1f6bc), Emojicon.fromCodePoint(0x1f6be), Emojicon.fromCodePoint(0x1f6b0), Emojicon.fromCodePoint(0x1f6ae), Emojicon.fromCodePoint(0x1f17f), Emojicon.fromChar((char) 0x267f), Emojicon.fromCodePoint(0x1f6ad), Emojicon.fromCodePoint(0x1f237), Emojicon.fromCodePoint(0x1f238), Emojicon.fromCodePoint(0x1f202), Emojicon.fromChar((char) 0x24c2), Emojicon.fromCodePoint(0x1f6c2), Emojicon.fromCodePoint(0x1f6c4), Emojicon.fromCodePoint(0x1f6c5), Emojicon.fromCodePoint(0x1f6c3), Emojicon.fromCodePoint(0x1f251), Emojicon.fromChar((char) 0x3299), Emojicon.fromChar((char) 0x3297), Emojicon.fromCodePoint(0x1f191), Emojicon.fromCodePoint(0x1f198), Emojicon.fromCodePoint(0x1f194), Emojicon.fromCodePoint(0x1f6ab), Emojicon.fromCodePoint(0x1f51e), Emojicon.fromCodePoint(0x1f4f5), Emojicon.fromCodePoint(0x1f6af), Emojicon.fromCodePoint(0x1f6b1), Emojicon.fromCodePoint(0x1f6b3), Emojicon.fromCodePoint(0x1f6b7), Emojicon.fromCodePoint(0x1f6b8), Emojicon.fromChar((char) 0x26d4), Emojicon.fromChar((char) 0x2733), Emojicon.fromChar((char) 0x2747), Emojicon.fromChar((char) 0x274e), Emojicon.fromChar((char) 0x2705), Emojicon.fromChar((char) 0x2734), Emojicon.fromCodePoint(0x1f49f), Emojicon.fromCodePoint(0x1f19a), Emojicon.fromCodePoint(0x1f4f3), Emojicon.fromCodePoint(0x1f4f4), Emojicon.fromCodePoint(0x1f170), Emojicon.fromCodePoint(0x1f171), Emojicon.fromCodePoint(0x1f18e), Emojicon.fromCodePoint(0x1f17e), Emojicon.fromCodePoint(0x1f4a0), Emojicon.fromChar((char) 0x27bf), Emojicon.fromChar((char) 0x267b), Emojicon.fromChar((char) 0x2648), Emojicon.fromChar((char) 0x2649), Emojicon.fromChar((char) 0x264a), Emojicon.fromChar((char) 0x264b), Emojicon.fromChar((char) 0x264c), Emojicon.fromChar((char) 0x264d), Emojicon.fromChar((char) 0x264e), Emojicon.fromChar((char) 0x264f), Emojicon.fromChar((char) 0x2650), Emojicon.fromChar((char) 0x2651), Emojicon.fromChar((char) 0x2652), Emojicon.fromChar((char) 0x2653), Emojicon.fromChar((char) 0x26ce), Emojicon.fromCodePoint(0x1f52f), Emojicon.fromCodePoint(0x1f3e7), Emojicon.fromCodePoint(0x1f4b9), Emojicon.fromCodePoint(0x1f4b2), Emojicon.fromCodePoint(0x1f4b1), // Emoji.fromChar((char)0x00a9), // Emoji.fromChar((char)0x00ae), Emojicon.fromChar((char) 0xe24e), Emojicon.fromChar((char) 0xe24f), Emojicon.fromChar((char) 0x2122), Emojicon.fromChar((char) 0x274c), Emojicon.fromChar((char) 0x203c), Emojicon.fromChar((char) 0x2049), Emojicon.fromChar((char) 0x2757), Emojicon.fromChar((char) 0x2753), Emojicon.fromChar((char) 0x2755), Emojicon.fromChar((char) 0x2754), Emojicon.fromChar((char) 0x2b55), Emojicon.fromCodePoint(0x1f51d), Emojicon.fromCodePoint(0x1f51a), Emojicon.fromCodePoint(0x1f519), Emojicon.fromCodePoint(0x1f51b), Emojicon.fromCodePoint(0x1f51c), Emojicon.fromCodePoint(0x1f503), Emojicon.fromCodePoint(0x1f55b), Emojicon.fromCodePoint(0x1f567), Emojicon.fromCodePoint(0x1f550), Emojicon.fromCodePoint(0x1f55c), Emojicon.fromCodePoint(0x1f551), Emojicon.fromCodePoint(0x1f55d), Emojicon.fromCodePoint(0x1f552), Emojicon.fromCodePoint(0x1f55e), Emojicon.fromCodePoint(0x1f553), Emojicon.fromCodePoint(0x1f55f), Emojicon.fromCodePoint(0x1f554), Emojicon.fromCodePoint(0x1f560), Emojicon.fromCodePoint(0x1f555), Emojicon.fromCodePoint(0x1f556), Emojicon.fromCodePoint(0x1f557), Emojicon.fromCodePoint(0x1f558), Emojicon.fromCodePoint(0x1f559), Emojicon.fromCodePoint(0x1f55a), Emojicon.fromCodePoint(0x1f561), Emojicon.fromCodePoint(0x1f562), Emojicon.fromCodePoint(0x1f563), Emojicon.fromCodePoint(0x1f564), Emojicon.fromCodePoint(0x1f565), Emojicon.fromCodePoint(0x1f566), Emojicon.fromChar((char) 0x2716), Emojicon.fromChar((char) 0x2795), Emojicon.fromChar((char) 0x2796), Emojicon.fromChar((char) 0x2797), Emojicon.fromChar((char) 0x2660), Emojicon.fromChar((char) 0x2665), Emojicon.fromChar((char) 0x2663), Emojicon.fromChar((char) 0x2666), Emojicon.fromCodePoint(0x1f4ae), Emojicon.fromCodePoint(0x1f4af), Emojicon.fromChar((char) 0x2714), Emojicon.fromChar((char) 0x2611), Emojicon.fromCodePoint(0x1f518), Emojicon.fromCodePoint(0x1f517), Emojicon.fromChar((char) 0x27b0), Emojicon.fromChar((char) 0x3030), Emojicon.fromChar((char) 0x303d), Emojicon.fromCodePoint(0x1f531), Emojicon.fromChar((char) 0x25fc), Emojicon.fromChar((char) 0x25fb), Emojicon.fromChar((char) 0x25fe), Emojicon.fromChar((char) 0x25fd), Emojicon.fromChar((char) 0x25aa), Emojicon.fromChar((char) 0x25ab), Emojicon.fromCodePoint(0x1f53a), Emojicon.fromCodePoint(0x1f532), Emojicon.fromCodePoint(0x1f533), Emojicon.fromChar((char) 0x26ab), Emojicon.fromChar((char) 0x26aa), Emojicon.fromCodePoint(0x1f534), Emojicon.fromCodePoint(0x1f535), Emojicon.fromCodePoint(0x1f53b), Emojicon.fromChar((char) 0x2b1c), Emojicon.fromChar((char) 0x2b1b), Emojicon.fromCodePoint(0x1f536), Emojicon.fromCodePoint(0x1f537), Emojicon.fromCodePoint(0x1f538), Emojicon.fromCodePoint(0x1f539), }; } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/pageView/QDEmojiconPagerView.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.fragment.components.qqface.pageView; import android.content.Context; import android.graphics.Color; import androidx.core.view.ViewCompat; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmuidemo.fragment.components.qqface.emojicon.EmojiconTextView; import com.qmuiteam.qmuidemo.R; /** * @author cginechen * @date 2017-06-08 */ public class QDEmojiconPagerView extends QDQQFaceBasePagerView { public QDEmojiconPagerView(Context context) { super(context); } @Override protected View getView(int position, View convertView, ViewGroup parent) { EmojiconTextView emojiconTextView; if (convertView == null || !(convertView instanceof EmojiconTextView)) { emojiconTextView = new EmojiconTextView(getContext()); emojiconTextView.setTextSize(14); int padding = QMUIDisplayHelper.dp2px(getContext(), 16); ViewCompat.setBackground(emojiconTextView, QMUIResHelper.getAttrDrawable( getContext(), R.attr.qmui_skin_support_s_list_item_bg_1)); emojiconTextView.setPadding(padding, padding, padding, padding); emojiconTextView.setMaxLines(8); emojiconTextView.setTextColor(Color.BLACK); emojiconTextView.setMovementMethodDefault(); convertView = emojiconTextView; } else { emojiconTextView = (EmojiconTextView) convertView; } emojiconTextView.setText(getItem(position)); return convertView; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/pageView/QDQQFaceBasePagerView.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.fragment.components.qqface.pageView; import android.content.Context; import androidx.core.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import com.qmuiteam.qmui.link.QMUIScrollingMovementMethod; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.fragment.components.qqface.QDQQFaceTestData; /** * @author cginechen * @date 2017-06-08 */ public abstract class QDQQFaceBasePagerView extends LinearLayout { private TextView mLogTv; private QDQQFaceTestData mTestData; public QDQQFaceBasePagerView(Context context) { super(context); mTestData = new QDQQFaceTestData(); setOrientation(VERTICAL); ListView listView = new ListView(context); LinearLayout.LayoutParams listLp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); listLp.weight = 1; listView.setLayoutParams(listLp); listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(new MyAdapter()); addView(listView); mLogTv = new TextView(context); LinearLayout.LayoutParams logLp = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, QMUIDisplayHelper.dp2px(context, 60)); mLogTv.setLayoutParams(logLp); mLogTv.setTextSize(12); mLogTv.setBackgroundResource(R.drawable.qmui_divider_top_bitmap); int paddingHor = QMUIDisplayHelper.dp2px(context, 16); mLogTv.setPadding(paddingHor, 0, paddingHor, 0); mLogTv.setTextColor(ContextCompat.getColor(context, R.color.qmui_config_color_black)); mLogTv.setMovementMethod(QMUIScrollingMovementMethod.getInstance()); addView(mLogTv); } protected CharSequence getItem(int position) { return mTestData.getList().get(position); } private void refreshLogView(String msg) { mLogTv.append(msg); int offset = mLogTv.getLineCount() * mLogTv.getLineHeight(); if (offset > mLogTv.getHeight()) { mLogTv.scrollTo(0, offset - mLogTv.getHeight()); } } protected abstract View getView(int position, View convertView, ViewGroup parent); class MyAdapter extends BaseAdapter { @Override public int getCount() { return mTestData.getList().size(); } @Override public CharSequence getItem(int position) { return mTestData.getList().get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { long start = System.currentTimeMillis(); convertView = QDQQFaceBasePagerView.this.getView(position, convertView, parent); long end = System.currentTimeMillis(); refreshLogView("getView : position = " + position + "; expend time = " + (end - start) + " \n"); return convertView; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/pageView/QDQQFacePagerView.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.fragment.components.qqface.pageView; import android.content.Context; import android.graphics.Color; import androidx.core.view.ViewCompat; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmuidemo.R; /** * @author cginechen * @date 2017-06-08 */ public class QDQQFacePagerView extends QDQQFaceBasePagerView { public QDQQFacePagerView(Context context) { super(context); } @Override protected View getView(int position, View convertView, ViewGroup parent) { QMUIQQFaceView qmuiqqFaceView; if (convertView == null || !(convertView instanceof QMUIQQFaceView)) { qmuiqqFaceView = new QMUIQQFaceView(getContext()); int padding = QMUIDisplayHelper.dp2px(getContext(), 16); ViewCompat.setBackground(qmuiqqFaceView, QMUIResHelper.getAttrDrawable( getContext(), R.attr.qmui_skin_support_s_list_item_bg_1)); qmuiqqFaceView.setPadding(padding, padding, padding, padding); qmuiqqFaceView.setLineSpace(QMUIDisplayHelper.dp2px(getContext(), 10)); qmuiqqFaceView.setTextColor(Color.BLACK); qmuiqqFaceView.setMaxLine(8); convertView = qmuiqqFaceView; } else { qmuiqqFaceView = (QMUIQQFaceView) convertView; } qmuiqqFaceView.setText(getItem(position)); return convertView; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDBaseSectionLayoutFragment.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.fragment.components.section; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import com.qmuiteam.qmui.recyclerView.QMUIRVDraggableScrollBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout; import com.qmuiteam.qmui.widget.section.QMUISection; import com.qmuiteam.qmui.widget.section.QMUIStickySectionAdapter; import com.qmuiteam.qmui.widget.section.QMUIStickySectionLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; import java.util.ArrayList; import butterknife.BindView; import butterknife.ButterKnife; public abstract class QDBaseSectionLayoutFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_to_refresh) QMUIPullRefreshLayout mPullRefreshLayout; @BindView(R.id.section_layout) QMUIStickySectionLayout mSectionLayout; private RecyclerView.LayoutManager mLayoutManager; protected QMUIStickySectionAdapter mAdapter; @Override protected View onCreateView() { View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_section_layout, null); ButterKnife.bind(this, view); initTopBar(); initRefreshLayout(); initStickyLayout(); initData(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheet(); } }); } private void initRefreshLayout() { mPullRefreshLayout.setOnPullListener(new QMUIPullRefreshLayout.OnPullListener() { @Override public void onMoveTarget(int offset) { } @Override public void onMoveRefreshView(int offset) { } @Override public void onRefresh() { mPullRefreshLayout.postDelayed(new Runnable() { @Override public void run() { mPullRefreshLayout.finishRefresh(); } }, 2000); } }); } protected void initStickyLayout() { mLayoutManager = createLayoutManager(); mSectionLayout.setLayoutManager(mLayoutManager); QMUIRVDraggableScrollBar scrollBar = new QMUIRVDraggableScrollBar(0, 0, 0); scrollBar.setEnableScrollBarFadeInOut(false); scrollBar.attachToStickSectionLayout(mSectionLayout); } private void initData() { mAdapter = createAdapter(); mAdapter.setCallback(new QMUIStickySectionAdapter.Callback() { @Override public void loadMore(final QMUISection section, final boolean loadMoreBefore) { mSectionLayout.postDelayed(new Runnable() { @Override public void run() { if (isAttachedToActivity()) { ArrayList list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(new SectionItem("load more item " + i)); } mAdapter.finishLoadMore(section, list, loadMoreBefore, false); } } }, 1000); } @Override public void onItemClick(QMUIStickySectionAdapter.ViewHolder holder, int position) { Toast.makeText(getContext(), "click item " + position, Toast.LENGTH_SHORT).show(); } @Override public boolean onItemLongClick(QMUIStickySectionAdapter.ViewHolder holder, int position) { Toast.makeText(getContext(), "long click item " + position, Toast.LENGTH_SHORT).show(); return true; } }); mSectionLayout.setAdapter(mAdapter, true); ArrayList> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(createSection("header " + i, i%2 != 0)); } mAdapter.setData(list); } private QMUISection createSection(String headerText, boolean isFold) { SectionHeader header = new SectionHeader(headerText); ArrayList contents = new ArrayList<>(); for (int i = 0; i < 20; i++) { contents.add(new SectionItem("item " + i)); } QMUISection section = new QMUISection<>(header, contents, isFold); // if test load more, you can open the code section.setExistAfterDataToLoad(true); // section.setExistBeforeDataToLoad(true); return section; } protected abstract QMUIStickySectionAdapter< SectionHeader, SectionItem, QMUIStickySectionAdapter.ViewHolder> createAdapter(); protected abstract RecyclerView.LayoutManager createLayoutManager(); private void showBottomSheet() { new QMUIBottomSheet.BottomListSheetBuilder(getContext()) .addItem("test scroll to section header") .addItem("test scroll to section item") .addItem("test find position") .addItem("test find custom position") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { switch (position) { case 0: { QMUISection section = mAdapter.getSectionDirectly(3); if (section != null) { mAdapter.scrollToSectionHeader(section, true); } break; } case 1: { QMUISection section = mAdapter.getSectionDirectly(3); if (section != null) { SectionItem item = section.getItemAt(10); if (item != null) { mAdapter.scrollToSectionItem(section, item, true); } } break; } case 2: { int targetPosition = mAdapter.findPosition(new QMUIStickySectionAdapter.PositionFinder() { @Override public boolean find(@NonNull QMUISection section, @Nullable SectionItem item) { return "header 4".equals(section.getHeader().getText()) && (item != null && "item 13".equals(item.getText())); } }, true); if (targetPosition != RecyclerView.NO_POSITION) { Toast.makeText(getContext(), "find position: " + targetPosition, Toast.LENGTH_SHORT).show(); QMUISection section = mAdapter.getSection(targetPosition); SectionItem item = mAdapter.getSectionItem(targetPosition); if (item != null) { mAdapter.scrollToSectionItem(section, item, true); } else if (section != null) { mAdapter.scrollToSectionHeader(section, true); } else { mLayoutManager.scrollToPosition(targetPosition); } } else { Toast.makeText(getContext(), "failed to find position", Toast.LENGTH_SHORT).show(); } break; } case 3: { int targetPosition = mAdapter.findCustomPosition(QMUISection.SECTION_INDEX_UNKNOWN, QDListWithDecorationSectionAdapter.ITEM_INDEX_LIST_FOOTER, false); if (targetPosition != RecyclerView.NO_POSITION) { Toast.makeText(getContext(), "find position: " + targetPosition, Toast.LENGTH_SHORT).show(); mLayoutManager.scrollToPosition(targetPosition); } } } dialog.dismiss(); } }) .build().show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionAdapter.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.fragment.components.section; import android.content.Context; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.section.QMUIDefaultStickySectionAdapter; import com.qmuiteam.qmui.widget.section.QMUISection; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; import com.qmuiteam.qmuidemo.view.QDLoadingItemView; import com.qmuiteam.qmuidemo.view.QDSectionHeaderView; public class QDGridSectionAdapter extends QMUIDefaultStickySectionAdapter { public QDGridSectionAdapter() { } public QDGridSectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { super(removeSectionTitleIfOnlyOneSection); } @NonNull @Override protected ViewHolder onCreateSectionHeaderViewHolder(@NonNull ViewGroup viewGroup) { return new ViewHolder(new QDSectionHeaderView(viewGroup.getContext())); } @NonNull @Override protected ViewHolder onCreateSectionItemViewHolder(@NonNull ViewGroup viewGroup) { Context context = viewGroup.getContext(); int paddingHor = QMUIDisplayHelper.dp2px(context, 24); int paddingVer = QMUIDisplayHelper.dp2px(context, 16); TextView tv = new TextView(context); tv.setTextSize(14); tv.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_gray_9)); tv.setTextColor(Color.DKGRAY); tv.setPadding(paddingHor, paddingVer, paddingHor, paddingVer); tv.setGravity(Gravity.CENTER); return new ViewHolder(tv); } @NonNull @Override protected ViewHolder onCreateSectionLoadingViewHolder(@NonNull ViewGroup viewGroup) { return new ViewHolder(new QDLoadingItemView(viewGroup.getContext())); } @Override protected void onBindSectionHeader(final ViewHolder holder, final int position, QMUISection section) { QDSectionHeaderView itemView = (QDSectionHeaderView) holder.itemView; itemView.render(section.getHeader(), section.isFold()); itemView.getArrowView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = holder.isForStickyHeader ? position : holder.getAdapterPosition(); toggleFold(pos, false); } }); } @Override protected void onBindSectionItem(ViewHolder holder, int position, QMUISection section, int itemIndex) { ((TextView) holder.itemView).setText(section.getItemAt(itemIndex).getText()); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDGridSectionLayoutFragment.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.fragment.components.section; import android.graphics.Rect; import androidx.annotation.NonNull; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.section.QMUIStickySectionAdapter; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; @Widget(group = Group.Other, name = "Sticky Section for Grid") public class QDGridSectionLayoutFragment extends QDBaseSectionLayoutFragment { @Override protected QMUIStickySectionAdapter createAdapter() { return new QDGridSectionAdapter(); } @Override protected RecyclerView.LayoutManager createLayoutManager() { final GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 3); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int i) { return mAdapter.getItemIndex(i) < 0 ? layoutManager.getSpanCount() : 1; } }); return layoutManager; } @Override protected void initStickyLayout() { super.initStickyLayout(); mSectionLayout.getRecyclerView().addItemDecoration(new RecyclerView.ItemDecoration() { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { if(view instanceof TextView){ int margin = QMUIDisplayHelper.dp2px(getContext(), 10); outRect.set(margin, margin, margin, margin); }else{ outRect.set(0, 0, 0, 0); } } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionAdapter.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.fragment.components.section; import android.content.Context; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import android.view.ViewGroup; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.R; public class QDListSectionAdapter extends QDGridSectionAdapter { public QDListSectionAdapter() { } public QDListSectionAdapter(boolean removeSectionTitleIfOnlyOneSection) { super(removeSectionTitleIfOnlyOneSection); } @NonNull @Override protected ViewHolder onCreateSectionItemViewHolder(@NonNull ViewGroup viewGroup) { Context context = viewGroup.getContext(); int paddingHor = QMUIDisplayHelper.dp2px(context, 24); int paddingVer = QMUIDisplayHelper.dp2px(context, 16); TextView tv = new TextView(context); tv.setTextSize(14); tv.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_gray_9)); tv.setTextColor(Color.DKGRAY); tv.setPadding(paddingHor, paddingVer, paddingHor, paddingVer); return new ViewHolder(tv); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListSectionLayoutFragment.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.fragment.components.section; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.ViewGroup; import com.qmuiteam.qmui.widget.section.QMUIStickySectionAdapter; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; @Widget(group = Group.Other, name = "Sticky Section for List") public class QDListSectionLayoutFragment extends QDBaseSectionLayoutFragment { @Override protected QMUIStickySectionAdapter createAdapter() { return new QDListSectionAdapter(true); } @Override protected RecyclerView.LayoutManager createLayoutManager() { return new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListWithDecorationSectionAdapter.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.fragment.components.section; import android.content.Context; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.section.QMUISection; import com.qmuiteam.qmui.widget.section.QMUISectionDiffCallback; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; import java.util.List; public class QDListWithDecorationSectionAdapter extends QDListSectionAdapter { public static final int ITEM_INDEX_LIST_HEADER = -1; public static final int ITEM_INDEX_LIST_FOOTER = -2; public static final int ITEM_INDEX_SECTION_TIP_START = -3; public static final int ITEM_INDEX_SECTION_TIP_END = -4; public static final int ITEM_TYPE_LIST_HEADER = 1; public static final int ITEM_TYPE_LIST_FOOTER = 2; public static final int ITEM_TYPE_SECTION_TIP_START = 3; public static final int ITEM_TYPE_SECTION_TIP_END = 4; @NonNull @Override protected ViewHolder onCreateCustomItemViewHolder(@NonNull ViewGroup viewGroup, int type) { View view; Context context = viewGroup.getContext(); if (type == ITEM_TYPE_LIST_HEADER) { ImageView iv = new ImageView(context); iv.setImageResource(R.mipmap.example_image2); view = iv; } else if (type == ITEM_TYPE_LIST_FOOTER) { TextView tv = new TextView(context); tv.setTextSize(12); tv.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_gray_9)); tv.setTextColor(Color.DKGRAY); tv.setText(R.string.sticky_section_decoration_list_footer); tv.setGravity(Gravity.CENTER); int paddingVer = QMUIDisplayHelper.dp2px(context, 16); tv.setPadding(0, paddingVer, 0, paddingVer); view = tv; } else if (type == ITEM_TYPE_SECTION_TIP_START) { TextView tv = new TextView(context); tv.setTextSize(12); tv.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_gray_9)); tv.setTextColor(Color.DKGRAY); tv.setText(R.string.sticky_section_decoration_section_top_tip); tv.setGravity(Gravity.CENTER); int paddingVer = QMUIDisplayHelper.dp2px(context, 16); tv.setPadding(0, paddingVer, 0, paddingVer); view = tv; } else if (type == ITEM_TYPE_SECTION_TIP_END) { TextView tv = new TextView(context); tv.setTextSize(12); tv.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_gray_9)); tv.setTextColor(Color.DKGRAY); tv.setText(R.string.sticky_section_decoration_section_bottom_tip); tv.setGravity(Gravity.CENTER); int paddingVer = QMUIDisplayHelper.dp2px(context, 16); tv.setPadding(0, paddingVer, 0, paddingVer); view = tv; } else { view = new View(viewGroup.getContext()); } return new ViewHolder(view); } @Override protected int getCustomItemViewType(int itemIndex, int position) { if (itemIndex == ITEM_INDEX_LIST_HEADER) { return ITEM_TYPE_LIST_HEADER; } else if (itemIndex == ITEM_INDEX_LIST_FOOTER) { return ITEM_TYPE_LIST_FOOTER; } else if (itemIndex == ITEM_INDEX_SECTION_TIP_START) { return ITEM_TYPE_SECTION_TIP_START; } else if (itemIndex == ITEM_INDEX_SECTION_TIP_END) { return ITEM_TYPE_SECTION_TIP_END; } return super.getCustomItemViewType(itemIndex, position); } @Override protected QMUISectionDiffCallback createDiffCallback( List> lastData, List> currentData) { return new QMUISectionDiffCallback(lastData, currentData) { @Override protected void onGenerateCustomIndexBeforeSectionList(IndexGenerationInfo generationInfo, List> list) { generationInfo.appendWholeListCustomIndex(ITEM_INDEX_LIST_HEADER); } @Override protected void onGenerateCustomIndexAfterSectionList(IndexGenerationInfo generationInfo, List> list) { generationInfo.appendWholeListCustomIndex(ITEM_INDEX_LIST_FOOTER); } @Override protected void onGenerateCustomIndexBeforeItemList(IndexGenerationInfo generationInfo, QMUISection section, int sectionIndex) { if (!section.isExistBeforeDataToLoad()) { generationInfo.appendCustomIndex(sectionIndex, ITEM_INDEX_SECTION_TIP_START); } } @Override protected void onGenerateCustomIndexAfterItemList(IndexGenerationInfo generationInfo, QMUISection section, int sectionIndex) { if (!section.isExistAfterDataToLoad()) { generationInfo.appendCustomIndex(sectionIndex, ITEM_INDEX_SECTION_TIP_END); } } @Override protected boolean areCustomContentsTheSame(@Nullable QMUISection oldSection, int oldItemIndex, @Nullable QMUISection newSection, int newItemIndex) { return true; } }; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDListWithDecorationSectionLayoutFragment.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.fragment.components.section; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.ViewGroup; import com.qmuiteam.qmui.widget.section.QMUIStickySectionAdapter; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.model.SectionHeader; import com.qmuiteam.qmuidemo.model.SectionItem; @Widget(group = Group.Other, name = "Sticky Section for List(With Decoration)") public class QDListWithDecorationSectionLayoutFragment extends QDBaseSectionLayoutFragment { @Override protected QMUIStickySectionAdapter createAdapter() { return new QDListWithDecorationSectionAdapter(); } @Override protected RecyclerView.LayoutManager createLayoutManager() { return new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/section/QDSectionLayoutFragment.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.fragment.components.section; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmui.widget.section.QMUIStickySectionLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIStickySectionLayout.class, iconRes = R.mipmap.icon_grid_sticky_section, docUrl = "https://github.com/Tencent/QMUI_Android/wiki/QMUIStickySectionLayout") public class QDSectionLayoutFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); injectDocToTopBar(mTopBar); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDListSectionLayoutFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDListSectionLayoutFragment fragment = new QDListSectionLayoutFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDGridSectionLayoutFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDGridSectionLayoutFragment fragment = new QDGridSectionLayoutFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDListWithDecorationSectionLayoutFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDListWithDecorationSectionLayoutFragment fragment = new QDListWithDecorationSectionLayoutFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeActionFragment.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.fragment.components.swipeAction; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.fragment.components.pullLayout.QDPullHorizontalTestFragment; import com.qmuiteam.qmuidemo.fragment.components.pullLayout.QDPullRefreshAndLoadMoreTestFragment; import com.qmuiteam.qmuidemo.fragment.components.pullLayout.QDPullVerticalTestFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(widgetClass = QMUIRVItemSwipeAction.class, iconRes = R.mipmap.icon_grid_rv_item_swipe_action) public class QDRVSwipeActionFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeMutiActionFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeMutiActionFragment fragment = new QDRVSwipeMutiActionFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeMutiActionOnlyIconFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeMutiActionOnlyIconFragment fragment = new QDRVSwipeMutiActionOnlyIconFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeMutiActionWithIconFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeMutiActionWithIconFragment fragment = new QDRVSwipeMutiActionWithIconFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeSingleDeleteActionFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeSingleDeleteActionFragment fragment = new QDRVSwipeSingleDeleteActionFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeDeleteWithNoActionFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeDeleteWithNoActionFragment fragment = new QDRVSwipeDeleteWithNoActionFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDRVSwipeUpDeleteFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDRVSwipeUpDeleteFragment fragment = new QDRVSwipeUpDeleteFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeDeleteWithNoActionFragment.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.fragment.components.swipeAction; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeAction; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Left: Delete With No Action") public class QDRVSwipeDeleteWithNoActionFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_LEFT; } @Override public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { super.onClickAction(swipeAction, selected, action); mAdapter.remove(selected.getAdapterPosition()); Toast.makeText(getContext(), "你点击了第 " + selected.getAdapterPosition() + " 个 item 的" + action.getText(), Toast.LENGTH_SHORT).show(); swipeAction.clear(); } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeMutiActionFragment.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.fragment.components.swipeAction; import android.content.Context; import android.graphics.Color; import android.icu.util.ValueIterator; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.recyclerView.QMUIRVDraggableScrollBar; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeViewHolder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Left: Muti Actions") @LatestVisitRecord public class QDRVSwipeMutiActionFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private Adapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_LEFT; } @Override public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { super.onClickAction(swipeAction, selected, action); Toast.makeText(getContext(), "你点击了第 " + selected.getAdapterPosition() + " 个 item 的" + action.getText(), Toast.LENGTH_SHORT).show(); if(action == mAdapter.mDeleteAction){ mAdapter.remove(selected.getAdapterPosition()); }else{ swipeAction.clear(); } } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new Adapter(getContext()); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } class Adapter extends RecyclerView.Adapter{ private List mData = new ArrayList<>(); final QMUISwipeAction mDeleteAction; final QMUISwipeAction mWriteReviewAction; public Adapter(Context context){ QMUISwipeAction.ActionBuilder builder = new QMUISwipeAction.ActionBuilder() .textSize(QMUIDisplayHelper.sp2px(context, 14)) .textColor(Color.WHITE) .paddingStartEnd(QMUIDisplayHelper.dp2px(getContext(), 14)); mDeleteAction = builder.text("删除").backgroundColor(Color.RED).build(); mWriteReviewAction = builder.text("写想法").backgroundColor(Color.BLUE).build(); } public void setData(@Nullable List list) { mData.clear(); if(list != null){ mData.addAll(list); } notifyDataSetChanged(); } public void remove(int pos){ mData.remove(pos); notifyItemRemoved(pos); } public void add(int pos, String item) { mData.add(pos, item); notifyItemInserted(pos); } public void prepend(@NonNull List items){ mData.addAll(0, items); notifyDataSetChanged(); } public void append(@NonNull List items){ mData.addAll(items); notifyDataSetChanged(); } @NonNull @Override public QMUISwipeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_1, parent, false); final QMUISwipeViewHolder vh = new QMUISwipeViewHolder(view); vh.addSwipeAction(mDeleteAction); vh.addSwipeAction(mWriteReviewAction); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), "click position=" + vh.getAdapterPosition(), Toast.LENGTH_SHORT).show(); } }); return vh; } @Override public void onBindViewHolder(@NonNull QMUISwipeViewHolder holder, int position) { TextView textView = holder.itemView.findViewById(R.id.text); textView.setText(mData.get(position)); } @Override public int getItemCount() { return mData.size(); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeMutiActionOnlyIconFragment.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.fragment.components.swipeAction; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeViewHolder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Left: Muti Actions With Only Icon") public class QDRVSwipeMutiActionOnlyIconFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private Adapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_LEFT; } @Override public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { super.onClickAction(swipeAction, selected, action); Toast.makeText(getContext(), "你点击了第 " + selected.getAdapterPosition() + " 个 item 的" + action.getText(), Toast.LENGTH_SHORT).show(); if(mAdapter.mAction1 == action){ mAdapter.remove(selected.getAdapterPosition()); }else{ swipeAction.clear(); } } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new Adapter(getContext()); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } class Adapter extends RecyclerView.Adapter{ private List mData = new ArrayList<>(); final QMUISwipeAction mAction1; final QMUISwipeAction mAction2; public Adapter(Context context){ QMUISwipeAction.ActionBuilder builder = new QMUISwipeAction.ActionBuilder() .textSize(QMUIDisplayHelper.sp2px(context, 14)) .textColor(Color.WHITE) .paddingStartEnd(QMUIDisplayHelper.dp2px(getContext(), 14)); mAction1 = builder .backgroundColor(Color.RED) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_delete_line)) .orientation(QMUISwipeAction.ActionBuilder.VERTICAL) .reverseDrawOrder(false) .build(); mAction2 = builder .backgroundColor(Color.BLUE) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_share)) .orientation(QMUISwipeAction.ActionBuilder.VERTICAL) .reverseDrawOrder(true) .build(); } public void setData(@Nullable List list) { mData.clear(); if(list != null){ mData.addAll(list); } notifyDataSetChanged(); } public void remove(int pos){ mData.remove(pos); notifyItemRemoved(pos); } public void add(int pos, String item) { mData.add(pos, item); notifyItemInserted(pos); } public void prepend(@NonNull List items){ mData.addAll(0, items); notifyDataSetChanged(); } public void append(@NonNull List items){ mData.addAll(items); notifyDataSetChanged(); } @NonNull @Override public QMUISwipeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_1, parent, false); final QMUISwipeViewHolder vh = new QMUISwipeViewHolder(view); vh.addSwipeAction(mAction1); vh.addSwipeAction(mAction2); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), "click position=" + vh.getAdapterPosition(), Toast.LENGTH_SHORT).show(); } }); return vh; } @Override public void onBindViewHolder(@NonNull QMUISwipeViewHolder holder, int position) { TextView textView = holder.itemView.findViewById(R.id.text); textView.setText(mData.get(position)); } @Override public int getItemCount() { return mData.size(); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeMutiActionWithIconFragment.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.fragment.components.swipeAction; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeViewHolder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Left: Muti Actions With Icon") public class QDRVSwipeMutiActionWithIconFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private Adapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_LEFT; } @Override public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { super.onClickAction(swipeAction, selected, action); Toast.makeText(getContext(), "你点击了第 " + selected.getAdapterPosition() + " 个 item 的" + action.getText(), Toast.LENGTH_SHORT).show(); if(mAdapter.mAction1 == action){ mAdapter.remove(selected.getAdapterPosition()); }else{ swipeAction.clear(); } } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new Adapter(getContext()); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } class Adapter extends RecyclerView.Adapter{ private List mData = new ArrayList<>(); final QMUISwipeAction mAction1; final QMUISwipeAction mAction2; final QMUISwipeAction mAction3; final QMUISwipeAction mAction4; public Adapter(Context context){ QMUISwipeAction.ActionBuilder builder = new QMUISwipeAction.ActionBuilder() .textSize(QMUIDisplayHelper.sp2px(context, 14)) .textColor(Color.WHITE) .paddingStartEnd(QMUIDisplayHelper.dp2px(getContext(), 14)); mAction1 = builder .text("删除") .backgroundColor(Color.RED) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_delete_line)) .orientation(QMUISwipeAction.ActionBuilder.VERTICAL) .reverseDrawOrder(false) .build(); mAction2 = builder .text("查词典") .backgroundColor(Color.BLUE) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_dict)) .orientation(QMUISwipeAction.ActionBuilder.VERTICAL) .reverseDrawOrder(true) .build(); mAction3 = builder .text("分享") .backgroundColor(Color.BLACK) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_share)) .orientation(QMUISwipeAction.ActionBuilder.HORIZONTAL) .reverseDrawOrder(false) .build(); mAction4 = builder .text("复制") .backgroundColor(Color.GRAY) .icon(ContextCompat.getDrawable(context, R.drawable.icon_quick_action_copy)) .orientation(QMUISwipeAction.ActionBuilder.HORIZONTAL) .reverseDrawOrder(true) .build(); } public void setData(@Nullable List list) { mData.clear(); if(list != null){ mData.addAll(list); } notifyDataSetChanged(); } public void remove(int pos){ mData.remove(pos); notifyItemRemoved(pos); } public void add(int pos, String item) { mData.add(pos, item); notifyItemInserted(pos); } public void prepend(@NonNull List items){ mData.addAll(0, items); notifyDataSetChanged(); } public void append(@NonNull List items){ mData.addAll(items); notifyDataSetChanged(); } @NonNull @Override public QMUISwipeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_1, parent, false); final QMUISwipeViewHolder vh = new QMUISwipeViewHolder(view); vh.addSwipeAction(mAction1); vh.addSwipeAction(mAction2); vh.addSwipeAction(mAction3); vh.addSwipeAction(mAction4); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), "click position=" + vh.getAdapterPosition(), Toast.LENGTH_SHORT).show(); } }); return vh; } @Override public void onBindViewHolder(@NonNull QMUISwipeViewHolder holder, int position) { TextView textView = holder.itemView.findViewById(R.id.text); textView.setText(mData.get(position)); } @Override public int getItemCount() { return mData.size(); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeSingleDeleteActionFragment.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.fragment.components.swipeAction; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeAction; import com.qmuiteam.qmui.recyclerView.QMUISwipeViewHolder; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Left: Single Action And Allow Deletion") public class QDRVSwipeSingleDeleteActionFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private Adapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_refresh_and_load_more_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(@NonNull QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_TOP) { onRefreshData(); } else if (pullAction.getPullEdge() == QMUIPullLayout.PULL_EDGE_BOTTOM) { onLoadMore(); } mPullLayout.finishActionRun(pullAction); } }, 3000); } }); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.remove(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_LEFT; } @Override public void onClickAction(QMUIRVItemSwipeAction swipeAction, RecyclerView.ViewHolder selected, QMUISwipeAction action) { super.onClickAction(swipeAction, selected, action); mAdapter.remove(selected.getAdapterPosition()); } }); swipeAction.attachToRecyclerView(mRecyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new Adapter(getContext()); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } private void onRefreshData() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onRefreshData-" + id + "-" + i); } mAdapter.prepend(data); mRecyclerView.scrollToPosition(0); } private void onLoadMore() { List data = new ArrayList<>(); long id = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { data.add("onLoadMore-" + id + "-" + i); } mAdapter.append(data); } class Adapter extends RecyclerView.Adapter { private List mData = new ArrayList<>(); private final QMUISwipeAction mDeleteAction; public Adapter(Context context) { QMUISwipeAction.ActionBuilder builder = new QMUISwipeAction.ActionBuilder() .textSize(QMUIDisplayHelper.sp2px(context, 14)) .textColor(Color.WHITE) .paddingStartEnd(QMUIDisplayHelper.dp2px(getContext(), 14)); mDeleteAction = builder.text("删除").backgroundColor(Color.RED).build(); } public void setData(@Nullable List list) { mData.clear(); if (list != null) { mData.addAll(list); } notifyDataSetChanged(); } public void remove(int pos) { mData.remove(pos); notifyItemRemoved(pos); } public void add(int pos, String item) { mData.add(pos, item); notifyItemInserted(pos); } public void prepend(@NonNull List items) { mData.addAll(0, items); notifyDataSetChanged(); } public void append(@NonNull List items) { mData.addAll(items); notifyDataSetChanged(); } @NonNull @Override public QMUISwipeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_1, parent, false); final QMUISwipeViewHolder vh = new QMUISwipeViewHolder(view); vh.addSwipeAction(mDeleteAction); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), "click position=" + vh.getAdapterPosition(), Toast.LENGTH_SHORT).show(); } }); return vh; } @Override public void onBindViewHolder(@NonNull QMUISwipeViewHolder holder, int position) { TextView textView = holder.itemView.findViewById(R.id.text); textView.setText(mData.get(position)); } @Override public int getItemCount() { return mData.size(); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/swipeAction/QDRVSwipeUpDeleteFragment.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.fragment.components.swipeAction; import android.app.Service; import android.os.Vibrator; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.PagerSnapHelper; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.QMUIInterpolatorStaticHolder; import com.qmuiteam.qmui.recyclerView.QMUIRVItemSwipeAction; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.pullLayout.QMUIPullLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Other, name = "Swipe Up: Long Press To Swipe Delete") public class QDRVSwipeUpDeleteFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pull_layout) QMUIPullLayout mPullLayout; @BindView(R.id.recyclerView) RecyclerView mRecyclerView; private QDRecyclerViewAdapter mAdapter; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pull_horizontal_test_layout, null); ButterKnife.bind(this, root); QDDataManager QDDataManager = com.qmuiteam.qmuidemo.manager.QDDataManager.getInstance(); mQDItemDescription = QDDataManager.getDescription(this.getClass()); initTopBar(); initData(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initData() { mPullLayout.setActionListener(new QMUIPullLayout.ActionListener() { @Override public void onActionTriggered(QMUIPullLayout.PullAction pullAction) { mPullLayout.postDelayed(new Runnable() { @Override public void run() { mPullLayout.finishActionRun(pullAction); } }, 1000); } }); LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false); mRecyclerView.setLayoutManager(layoutManager); new PagerSnapHelper().attachToRecyclerView(mRecyclerView); mAdapter = new QDRecyclerViewAdapter(); mAdapter.setItemCount(10); mRecyclerView.setAdapter(mAdapter); QMUIRVItemSwipeAction swipeAction = new QMUIRVItemSwipeAction(true, new QMUIRVItemSwipeAction.Callback() { @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { mAdapter.removeItem(viewHolder.getAdapterPosition()); } @Override public int getSwipeDirection(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return QMUIRVItemSwipeAction.SWIPE_UP; } @Override public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return 0.3f; } @Override public void onSelectedChanged(RecyclerView.ViewHolder selected) { super.onSelectedChanged(selected); if (selected != null) { mTopBar.setTitle("上滑删除"); selected.itemView.animate() .scaleX(1.02f) .scaleY(1.02f) .setInterpolator(QMUIInterpolatorStaticHolder.ACCELERATE_INTERPOLATOR) .setDuration(250) .start(); // 震动 Vibrator vibrator = (Vibrator) getContext().getSystemService(Service.VIBRATOR_SERVICE); vibrator.vibrate(10); } else { mTopBar.setTitle(mQDItemDescription.getName()); } } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); View itemView = viewHolder.itemView; if (itemView.getScaleX() != 1f || itemView.getScaleY() != 1f) { itemView.animate() .scaleX(1f) .scaleY(1f) .setInterpolator(QMUIInterpolatorStaticHolder.DECELERATE_INTERPOLATOR) .setDuration(250) .start(); } else { itemView.animate().cancel(); } } }); swipeAction.setPressTimeToSwipe(300); swipeAction.attachToRecyclerView(mRecyclerView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/CardTransformer.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.fragment.components.viewpager; import android.view.View; import androidx.viewpager.widget.ViewPager; /** * @author cginechen * @date 2017-09-13 */ public class CardTransformer implements ViewPager.PageTransformer { @Override public void transformPage(View page, float position) { // 刷新数据notifyDataSetChange之后也会调用到transformPage,但此时的position可能不在[-1, 1]之间 if (position <= -1 || position >= 1f) { page.setRotation(0); } else { page.setRotation(position * 30); page.setPivotX(page.getWidth() * .5f); page.setPivotY(page.getHeight() * 1f); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDFitSystemWindowViewPagerFragment.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.fragment.components.viewpager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.QMUIFragmentPagerAdapter; import com.qmuiteam.qmui.widget.QMUIViewPager; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.fragment.components.QDCollapsingTopBarLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentScrollableModeFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-09-13 */ @Widget(name = "QDFitSystemWindowViewPagerFragment") public class QDFitSystemWindowViewPagerFragment extends BaseFragment { @BindView(R.id.pager) QMUIViewPager mViewPager; @BindView(R.id.tabs) QMUITabSegment mTabSegment; @Override protected View onCreateView() { FrameLayout layout = (FrameLayout) LayoutInflater.from(getActivity()).inflate(R.layout.fragment_fsw_viewpager, null); ButterKnife.bind(this, layout); initPagers(); return layout; } private void initPagers() { QMUIFragmentPagerAdapter pagerAdapter = new QMUIFragmentPagerAdapter(getChildFragmentManager()) { @Override public QMUIFragment createFragment(int position) { switch (position) { case 0: return new QDTabSegmentScrollableModeFragment(); case 1: return new QDCollapsingTopBarLayoutFragment(); case 2: return new QDFitSystemWindowViewPagerFragment(); case 3: default: return new QDViewPagerFragment(); } } @Override public int getCount() { return 4; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return "TabSegment"; case 1: return "CTopBar"; case 2: return "IViewPager"; case 3: default: return "ViewPager"; } } }; mViewPager.setAdapter(pagerAdapter); mTabSegment.setupWithViewPager(mViewPager); } @Override protected boolean canDragBack() { return mViewPager.getCurrentItem() == 0; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDLoopViewPagerFragment.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.fragment.components.viewpager; import android.content.Context; import android.os.Build; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUIPagerAdapter; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUIViewPager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import java.util.ArrayList; import java.util.List; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-09-13 */ @Widget(name = "QDLoopViewPagerFragment") public class QDLoopViewPagerFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBar mTopBar; @BindView(R.id.pager) QMUIViewPager mViewPager; private List mItems = new ArrayList<>(); @Override protected View onCreateView() { FrameLayout layout = (FrameLayout) LayoutInflater.from(getActivity()).inflate(R.layout.fragment_loop_viewpager, null); ButterKnife.bind(this, layout); initData(5); initTopBar(); initPagers(); return layout; } private void initData(int count) { for (int i = 0; i < count; i++) { mItems.add(String.valueOf(i)); } } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); } private void initPagers() { QMUIPagerAdapter pagerAdapter = new QMUIPagerAdapter() { @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } @Override public int getCount() { return mItems.size(); } @Override public CharSequence getPageTitle(int position) { return mItems.get(position); } @Override @NonNull protected Object hydrate(@NonNull ViewGroup container, int position) { return new ItemView(getContext()); } @Override protected void populate(@NonNull ViewGroup container, @NonNull Object item, int position) { ItemView itemView = (ItemView) item; itemView.setText(mItems.get(position)); container.addView(itemView); } @Override protected void destroy(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); } }; //setPageTransformer默认采用ViewCompat.LAYER_TYPE_HARDWARE, 但它在某些4.x的国产机下会crash boolean canUseHardware = Build.VERSION.SDK_INT >= 21; mViewPager.setPageTransformer(false, new CardTransformer(), canUseHardware ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_SOFTWARE); mViewPager.setInfiniteRatio(500); mViewPager.setEnableLoop(true); mViewPager.setAdapter(pagerAdapter); } static class ItemView extends FrameLayout { private TextView mTextView; public ItemView(Context context) { super(context); mTextView = new TextView(context); mTextView.setTextSize(20); mTextView.setTextColor(ContextCompat.getColor(context, R.color.app_color_theme_5)); mTextView.setGravity(Gravity.CENTER); mTextView.setBackgroundColor(ContextCompat.getColor(context, R.color.qmui_config_color_white)); int size = QMUIDisplayHelper.dp2px(context, 300); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size, size); lp.gravity = Gravity.CENTER; addView(mTextView, lp); } public void setText(CharSequence text) { mTextView.setText(text); } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/viewpager/QDViewPagerFragment.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.fragment.components.viewpager; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.QMUIViewPager; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-09-13 */ @Widget(widgetClass = QMUIViewPager.class, iconRes = R.mipmap.icon_grid_pager_layout_manager) public class QDViewPagerFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDFitSystemWindowViewPagerFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDFitSystemWindowViewPagerFragment fragment = new QDFitSystemWindowViewPagerFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDLoopViewPagerFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDLoopViewPagerFragment fragment = new QDLoopViewPagerFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeComponentsController.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.fragment.home; import android.content.Context; import com.qmuiteam.qmuidemo.manager.QDDataManager; /** * @author cginechen * @date 2016-10-20 */ public class HomeComponentsController extends HomeController { public HomeComponentsController(Context context) { super(context); } @Override protected String getTitle() { return "Components"; } @Override protected ItemAdapter getItemAdapter() { return new ItemAdapter(getContext(), QDDataManager.getInstance().getComponentsDescriptions()); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeController.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.fragment.home; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Parcelable; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.decorator.GridDividerItemDecoration; import com.qmuiteam.qmuidemo.fragment.QDAboutFragment; import com.qmuiteam.qmuidemo.fragment.util.QDNotchHelperFragment; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.List; /** * @author cginechen * @date 2016-10-20 */ public abstract class HomeController extends LinearLayout { protected QMUITopBarLayout mTopBar; protected RecyclerView mRecyclerView; private HomeControlListener mHomeControlListener; private ItemAdapter mItemAdapter; private int mDiffRecyclerViewSaveStateId = QMUIViewHelper.generateViewId(); public HomeController(Context context) { super(context); setOrientation(LinearLayout.VERTICAL); mTopBar = new QMUITopBarLayout(context); mTopBar.setId(View.generateViewId()); mTopBar.setFitsSystemWindows(true); mRecyclerView = new RecyclerView(context); mRecyclerView.setId(View.generateViewId()); addView(mTopBar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); addView(mRecyclerView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0, 1f)); initTopBar(); initRecyclerView(); } protected void startFragment(BaseFragment fragment) { if (mHomeControlListener != null) { mHomeControlListener.startFragment(fragment); } } public void setHomeControlListener(HomeControlListener homeControlListener) { mHomeControlListener = homeControlListener; } protected abstract String getTitle(); private void initTopBar() { mTopBar.setTitle(getTitle()); mTopBar.addRightImageButton(R.mipmap.icon_topbar_about, R.id.topbar_right_about_button).setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { QDAboutFragment fragment = new QDAboutFragment(); startFragment(fragment); } }); } private void initRecyclerView() { mItemAdapter = getItemAdapter(); mItemAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { QDItemDescription item = mItemAdapter.getItem(pos); try { BaseFragment fragment = item.getDemoClass().newInstance(); if (fragment instanceof QDNotchHelperFragment) { Context context = getContext(); Intent intent = QDMainActivity.of(context, QDNotchHelperFragment.class); context.startActivity(intent); if (context instanceof Activity) { ((Activity) context).overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); } } else { startFragment(fragment); } } catch (Exception e) { e.printStackTrace(); } } }); mRecyclerView.setAdapter(mItemAdapter); int spanCount = 3; mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount)); mRecyclerView.addItemDecoration(new GridDividerItemDecoration(getContext(), spanCount)); } protected abstract ItemAdapter getItemAdapter(); public interface HomeControlListener { void startFragment(BaseFragment fragment); } @Override protected void dispatchSaveInstanceState(SparseArray container) { int id = mRecyclerView.getId(); mRecyclerView.setId(mDiffRecyclerViewSaveStateId); super.dispatchSaveInstanceState(container); mRecyclerView.setId(id); } @Override protected void dispatchRestoreInstanceState(SparseArray container) { int id = mRecyclerView.getId(); mRecyclerView.setId(mDiffRecyclerViewSaveStateId); super.dispatchRestoreInstanceState(container); mRecyclerView.setId(id); } static class ItemAdapter extends BaseRecyclerAdapter { public ItemAdapter(Context ctx, List data) { super(ctx, data); } @Override public int getItemLayoutId(int viewType) { return R.layout.home_item_layout; } @Override public void bindData(RecyclerViewHolder holder, int position, QDItemDescription item) { holder.getTextView(R.id.item_name).setText(item.getName()); if (item.getIconRes() != 0) { holder.getImageView(R.id.item_icon).setImageResource(item.getIconRes()); } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeFragment.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.fragment.home; import android.content.Context; import android.graphics.Typeface; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.qmuiteam.qmui.arch.effect.QMUIFragmentEffectHandler; import com.qmuiteam.qmui.arch.effect.QMUIFragmentMapEffectHandler; import com.qmuiteam.qmui.arch.effect.MapEffect; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.tab.QMUITab; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.model.CustomEffect; import java.util.HashMap; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2016-10-19 */ public class HomeFragment extends BaseFragment { private final static String TAG = HomeFragment.class.getSimpleName(); @BindView(R.id.pager) ViewPager mViewPager; @BindView(R.id.tabs) QMUITabSegment mTabSegment; private HashMap mPages; private PagerAdapter mPagerAdapter = new PagerAdapter() { private int mChildCount = 0; @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public int getCount() { return mPages.size(); } @Override public Object instantiateItem(final ViewGroup container, int position) { HomeController page = mPages.get(Pager.getPagerFromPosition(position)); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.addView(page, params); return page; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public int getItemPosition(Object object) { if (mChildCount == 0) { return POSITION_NONE; } return super.getItemPosition(object); } @Override public void notifyDataSetChanged() { mChildCount = getCount(); super.notifyDataSetChanged(); } }; @Override public void onAttach(@NonNull Context context) { super.onAttach(context); registerEffect(this, new QMUIFragmentMapEffectHandler() { @Override public boolean shouldHandleEffect(@NonNull MapEffect effect) { return effect.getValue("interested_type_key") != null; } @Override public void handleEffect(@NonNull MapEffect effect) { Object value = effect.getValue("interested_value_key"); if(value instanceof String){ Toast.makeText(context, ((String)value), Toast.LENGTH_SHORT).show(); } } }); registerEffect(this, new QMUIFragmentEffectHandler() { @Override public boolean shouldHandleEffect(@NonNull CustomEffect effect) { return true; } @Override public void handleEffect(@NonNull CustomEffect effect) { Toast.makeText(context, effect.getContent(), Toast.LENGTH_SHORT).show(); } @Override public void handleEffect(@NonNull List effects) { // we can only handle the last effect. handleEffect(effects.get(effects.size() - 1)); } }); } @Override protected View onCreateView() { FrameLayout layout = (FrameLayout) LayoutInflater.from(getActivity()).inflate(R.layout.fragment_home, null); ButterKnife.bind(this, layout); initTabs(); initPagers(); return layout; } private void initTabs() { QMUITabBuilder builder = mTabSegment.tabBuilder(); builder.setTypeface(null, Typeface.DEFAULT_BOLD); builder.setSelectedIconScale(1.2f) .setTextSize(QMUIDisplayHelper.sp2px(getContext(), 13), QMUIDisplayHelper.sp2px(getContext(), 15)) .setDynamicChangeIconColor(false); QMUITab component = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); QMUITab lab = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_lab)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_lab_selected)) .setText("Lab") .build(getContext()); mTabSegment.addTab(component) .addTab(util) .addTab(lab); } private void initPagers() { HomeController.HomeControlListener listener = new HomeController.HomeControlListener() { @Override public void startFragment(BaseFragment fragment) { HomeFragment.this.startFragment(fragment); } }; mPages = new HashMap<>(); HomeController homeComponentsController = new HomeComponentsController(getActivity()); homeComponentsController.setHomeControlListener(listener); mPages.put(Pager.COMPONENT, homeComponentsController); HomeController homeUtilController = new HomeUtilController(getActivity()); homeUtilController.setHomeControlListener(listener); mPages.put(Pager.UTIL, homeUtilController); HomeController homeLabController = new HomeLabController(getActivity()); homeLabController.setHomeControlListener(listener); mPages.put(Pager.LAB, homeLabController); mViewPager.setAdapter(mPagerAdapter); mTabSegment.setupWithViewPager(mViewPager, false); } enum Pager { COMPONENT, UTIL, LAB; public static Pager getPagerFromPosition(int position) { switch (position) { case 0: return COMPONENT; case 1: return UTIL; case 2: return LAB; default: return COMPONENT; } } } @Override protected boolean canDragBack() { return false; } @Override public Object onLastFragmentFinish() { return null; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeLabController.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.fragment.home; import android.content.Context; import com.qmuiteam.qmuidemo.manager.QDDataManager; /** * @author cginechen * @date 2016-10-20 */ public class HomeLabController extends HomeController { public HomeLabController(Context context) { super(context); } @Override protected String getTitle() { return "Lab"; } @Override protected ItemAdapter getItemAdapter() { return new ItemAdapter(getContext(), QDDataManager.getInstance().getLabDescriptions()); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/home/HomeUtilController.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.fragment.home; import android.content.Context; import com.qmuiteam.qmuidemo.manager.QDDataManager; /** 主界面,关于 QMUI Util 部分的展示。 * Created by Kayo on 2016/11/21. */ public class HomeUtilController extends HomeController { public HomeUtilController(Context context) { super(context); } @Override protected String getTitle() { return "Helper"; } @Override protected ItemAdapter getItemAdapter() { return new ItemAdapter(getContext(), QDDataManager.getInstance().getUtilDescriptions()); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDAnimationListViewFragment.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.fragment.lab; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUIAnimationListView; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.adaptor.QDSimpleAdapter; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-03-30 */ @Widget(group = Group.Lab, widgetClass = QMUIAnimationListView.class, iconRes = R.mipmap.icon_grid_anim_list_view) public class QDAnimationListViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.listview) QMUIAnimationListView mListView; private List mData = new ArrayList<>(); @Override protected View onCreateView() { View root = LayoutInflater.from(getContext()).inflate(R.layout.fragment_animation_listview, null); ButterKnife.bind(this, root); initTopBar(); initListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); mTopBar.addRightTextButton("添加", QMUIViewHelper.generateViewId()).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListView.manipulate(new QMUIAnimationListView.Manipulator() { @Override public void manipulate(MyAdapter adapter) { int position = mListView.getFirstVisiblePosition(); long current = System.currentTimeMillis(); mData.add(position + 1, "item add" + (current + 1)); mData.add(position + 2, "item add" + (current + 2)); mData.add(position + 3, "item add" + (current + 3)); } }); } }); mTopBar.addRightTextButton("删除", QMUIViewHelper.generateViewId()).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListView.manipulate(new QMUIAnimationListView.Manipulator() { @Override public void manipulate(MyAdapter adapter) { int position = mListView.getFirstVisiblePosition(); if(mData.size() > position + 4){ mData.remove(position + 1); mData.remove(position + 3); }else{ Toast.makeText(getContext(), "item 已经很少了,不如先添加几个?", Toast.LENGTH_SHORT).show(); } } }); } }); } private void initListView() { for (int i = 0; i < 20; i++) { mData.add("item " + (i + 1)); } MyAdapter adapter = new MyAdapter(getContext(), mData); mListView.setAdapter(adapter); } private static class MyAdapter extends QDSimpleAdapter { public MyAdapter(Context context, List data) { super(context, data); } @Override public long getItemId(int position) { return getItem(position).hashCode(); } @Override public boolean hasStableIds() { return true; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchNavFragment.java ================================================ package com.qmuiteam.qmuidemo.fragment.lab; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentContainerView; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.QMUINavFragment; import com.qmuiteam.qmui.arch.SwipeBackLayout; import com.qmuiteam.qmui.arch.record.RecordArgumentEditor; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.fragment.home.HomeFragment; public class QDArchNavFragment extends QMUINavFragment { private static final String TAG = "QDArchNavFragment"; public static QMUINavFragment getInstance(Class firstClass, @Nullable Bundle bundle) { QMUINavFragment navFragment = new QDArchNavFragment(); navFragment.setArguments(initArguments(firstClass, bundle)); return navFragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle bundle = getArguments(); Log.i(TAG, "1"); if(bundle != null){ String navTest = bundle.getString("nav_test"); if(navTest != null){ Log.i(TAG, "latestVisit: " + navTest); } } } @Override protected View onCreateView() { FrameLayout root = new FrameLayout(getContext()); FragmentContainerView fragmentContainerView = new FragmentContainerView(getContext()); TextView tipView = new TextView(getContext()); tipView.setText("Nav"); tipView.setBackgroundColor(Color.RED); tipView.setTextColor(Color.WHITE); root.addView(fragmentContainerView); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; root.addView(tipView, lp); configFragmentContainerView(fragmentContainerView); return root; } @Override public void onCollectLatestVisitArgument(RecordArgumentEditor editor) { editor.putString("nav_test", "nav_test"); } @Override public Object onLastFragmentFinish() { return new HomeFragment(); } @Override protected int backViewInitOffset(Context context, int dragDirection, int moveEdge) { if (moveEdge == SwipeBackLayout.EDGE_TOP || moveEdge == SwipeBackLayout.EDGE_BOTTOM) { return 0; } return QMUIDisplayHelper.dp2px(context, 100); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchSurfaceTestFragment.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.fragment.lab; import android.opengl.GLSurfaceView; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import butterknife.BindView; import butterknife.ButterKnife; import static android.opengl.GLES10.glClearColor; import static android.opengl.GLES20.glViewport; //TODO xiaomi 8 surfaceView can not move when swipe back. It's ok in pixel public class QDArchSurfaceTestFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.container) FrameLayout mContainer; private GLSurfaceView mSurfaceView; @Override protected View onCreateView() { View view = LayoutInflater.from(getContext()) .inflate(R.layout.fragment_surface_test, null); ButterKnife.bind(this, view); mSurfaceView = new GLSurfaceView(getContext()); mSurfaceView.setEGLContextClientVersion(2); mSurfaceView.setRenderer(new GLSurfaceView.Renderer() { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { glClearColor(0, 0, 0, 0); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 gl) { } }); mContainer.addView(mSurfaceView); initTopBar(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle("Test SurfaceView"); QDArchTestFragment.injectEntrance(mTopBar); } @Override public void onResume() { super.onResume(); mSurfaceView.onResume(); } @Override public void onPause() { super.onPause(); mSurfaceView.onPause(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchTestFragment.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.fragment.lab; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qmuiteam.qmui.arch.QMUIFragment; import com.qmuiteam.qmui.arch.QMUINavFragment; import com.qmuiteam.qmui.arch.SwipeBackLayout; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.arch.record.RecordArgumentEditor; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.activity.ArchTestActivity; import com.qmuiteam.qmuidemo.activity.TestArchInViewPagerActivity; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import butterknife.BindView; import butterknife.ButterKnife; @Widget(name = "QMUIFragment", iconRes = R.mipmap.icon_grid_layout) @LatestVisitRecord public class QDArchTestFragment extends BaseFragment { private static final String TAG = "QDArchTestFragment"; private static final String ARG_INDEX = "arg_index"; private static final int REQUEST_CODE = 1; private static final String DATA_TEST = "data_test"; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.title) TextView mTitleTv; @BindView(R.id.btn) QMUIRoundButton mBtn; @BindView(R.id.btn_1) QMUIRoundButton mBtn1; @BindView(R.id.btn_2) QMUIRoundButton mBtn2; @BindView(R.id.btn_3) QMUIRoundButton mBtn3; private Holder mHolder = new Holder(); @Override protected View onCreateView() { Bundle args = getArguments(); final int index = args == null ? 1 : args.getInt(ARG_INDEX); View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_arch_test, null); ButterKnife.bind(this, view); mHolder.mTestView = view.findViewById(R.id.test); mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); injectEntrance(mTopBar); mTopBar.setTitle(String.valueOf(index)); mTitleTv.setText(String.valueOf(index)); final int next = index + 1; final boolean destroyCurrent = next % 3 == 0; String btnText = destroyCurrent ? "startFragmentAndDestroyCurrent" : "startFragment"; mBtn.setText(btnText); mBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUIFragment fragment = newInstance(next); if (destroyCurrent) { startFragmentAndDestroyCurrent(fragment); } else { startFragmentForResult(fragment, REQUEST_CODE); } } }); mBtn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); Intent intent = QDMainActivity.of(getContext(), QDArchTestFragment.class); startActivity(intent); } }); mBtn2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUINavFragment fragment = QDArchNavFragment.getInstance(QDArchTestFragment.class, null); startFragment(fragment); } }); mBtn3.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(getParentFragment() instanceof QMUIFragment){ ((QMUIFragment)getParentFragment()).startFragment(newInstance(next)); } } }); return view; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = new Intent(); intent.putExtra(DATA_TEST, "test"); setFragmentResult(RESULT_OK, intent); Bundle arguments = getArguments(); if (arguments != null && arguments.getLong("test_long") == 100 && arguments.getLong("test_long1") == 1000 && arguments.getLong("test_long2") == 400 && arguments.getLong("test_long3", 200) == 200 && arguments.getFloat("test_float") == 100.13f && "你好".equals(arguments.getString("test_string"))) { Toast.makeText(getContext(), "恢复到最近阅读(Muti)", Toast.LENGTH_SHORT).show(); } } @Override public void onCollectLatestVisitArgument(RecordArgumentEditor editor) { editor.putLong("test_long", 100L); editor.putLong("test_long1", 1000); editor.putLong("test_long2", 400); editor.putString("test_string", "你好"); editor.putFloat("test_float", 100.13f); } public static QDArchTestFragment newInstance(int index) { Bundle args = new Bundle(); args.putInt(ARG_INDEX, index); QDArchTestFragment fragment = new QDArchTestFragment(); fragment.setArguments(args); return fragment; } @Override protected void onFragmentResult(int requestCode, int resultCode, Intent data) { super.onFragmentResult(requestCode, resultCode, data); if (data != null) { Log.i(TAG, data.getStringExtra(DATA_TEST)); } } @Override protected int getDragDirection(@NonNull SwipeBackLayout swipeBackLayout, @NonNull SwipeBackLayout.ViewMoveAction viewMoveAction, float downX, float downY, float dx, float dy, float slopTouch) { if(dx >= slopTouch){ return SwipeBackLayout.DRAG_DIRECTION_LEFT_TO_RIGHT; }else if(-dx>= slopTouch){ return SwipeBackLayout.DRAG_DIRECTION_RIGHT_TO_LEFT; } else if(dy >= slopTouch){ return SwipeBackLayout.DRAG_DIRECTION_TOP_TO_BOTTOM; }else if(-dy >= slopTouch){ return SwipeBackLayout.DRAG_DIRECTION_BOTTOM_TO_TOP; } return SwipeBackLayout.DRAG_DIRECTION_NONE; } public static void injectEntrance(final QMUITopBarLayout topbar) { topbar.addRightTextButton("new Activity", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBottomSheetList(topbar.getContext()); } }); } public static void showBottomSheetList(final Context context) { new QMUIBottomSheet.BottomListSheetBuilder(context) .addItem("Normal Arch Test") .addItem("WebView Test") .addItem("SurfaceView Test") .addItem("Directly Activity") .addItem("Directly Activity And Keep Bottom Sheet shown") .addItem("Show a Dialog") .addItem("QMUIFragment in QMUIActivity") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { if (position != 4) { dialog.dismiss(); } if (position == 0) { Intent intent = QDMainActivity.of(context, QDArchTestFragment.class); context.startActivity(intent); } else if (position == 1) { Intent intent = QDMainActivity.createWebExplorerIntent(context, "https://github.com/Tencent/QMUI_Android", context.getResources().getString(R.string.about_item_github)); context.startActivity(intent); } else if (position == 2) { Intent intent = QDMainActivity.of(context, QDArchSurfaceTestFragment.class); context.startActivity(intent); } else if (position == 3) { Intent intent = new Intent(context, ArchTestActivity.class); context.startActivity(intent); } else if (position == 4) { Intent intent = new Intent(context, ArchTestActivity.class); context.startActivity(intent); } else if (position == 5) { new QMUIDialog.MessageDialogBuilder(context) .setMessage("click ok to go new activity. then swipe back, " + "we should also see this dialog") .addAction(R.string.cancel, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { dialog.dismiss(); } }) .addAction(R.string.ok, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { Intent intent = new Intent(context, ArchTestActivity.class); context.startActivity(intent); } }) .show(); } else if (position == 6) { Intent intent = new Intent(context, TestArchInViewPagerActivity.class); context.startActivity(intent); } } }) .build() .show(); } static class Holder { TextView mTestView; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDArchWebViewTestFragment.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.fragment.lab; import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment; public class QDArchWebViewTestFragment extends QDWebExplorerFragment { @Override protected void initTopbar() { super.initTopbar(); // for test // QDArchTestFragment.injectEntrance(mTopBarLayout); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDComposeTipFragment.kt ================================================ package com.qmuiteam.qmuidemo.fragment.lab import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import com.qmuiteam.compose.core.ui.QMUITopBarBackIconItem import com.qmuiteam.compose.core.ui.QMUITopBarWithLazyScrollState import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.ComposeBaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget @Widget(name = "QMUI Compose Tip", iconRes = R.mipmap.icon_grid_in_progress) @LatestVisitRecord class QDComposeTipFragment : ComposeBaseFragment() { @Composable override fun PageContent() { Column(modifier = Modifier.fillMaxSize()) { val scrollState = rememberLazyListState() QMUITopBarWithLazyScrollState( scrollState = scrollState, title = "QMUIPhoto", leftItems = arrayListOf( QMUITopBarBackIconItem { popBackStack() } ) ) LazyColumn( state = scrollState, modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White) ) { item { Column( modifier = Modifier .fillMaxWidth() .padding(12.dp) ) { Text( text = "在 UI 开发过程中,经常会遇到如下一个需求:\n" + "假设一个布局是 【头像】【人名】【推荐信息】,正常用 LinearLayout 实现, " + "是没有任何问题的,但是要求在人名过长,整体内容会超过容器宽度时," + "不要省略推荐信息,而是省略人名信息。", fontSize = 13.sp, modifier = Modifier.padding(vertical = 12.dp) ) ConstraintLayout( modifier = Modifier .fillMaxWidth() .height(100.dp) .background(Color.LightGray) ) { val (one, two, three, four) = createRefs() val horChain = createHorizontalChain(one, two, three, chainStyle = ChainStyle.Packed(0f)) constrain(horChain) { start.linkTo(parent.start) end.linkTo(four.start) } Text( "此处不压缩", color = Color.White, maxLines = 1, modifier = Modifier .background(Color.Red) .constrainAs(one) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) }) Text( "此处如果内容有那么一点点过长,那就压缩省略压缩省略压缩省略", color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier .background(Color.Green) .constrainAs(two) { width = Dimension.preferredWrapContent top.linkTo(parent.top) bottom.linkTo(parent.bottom) }) Text( "此处也不压缩", color = Color.White, maxLines = 1, modifier = Modifier .background(Color.Black) .constrainAs(three) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) }) Box( modifier = Modifier .fillMaxHeight() .width(50.dp) .background(Color.Blue) .constrainAs(four) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) end.linkTo(parent.end) } ) } } } } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousBottomView.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.fragment.lab; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.IQMUIContinuousNestedBottomView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomDelegateLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUIPagerAdapter; import com.qmuiteam.qmui.widget.QMUIViewPager; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; public class QDContinuousBottomView extends QMUIContinuousNestedBottomDelegateLayout { private MyViewPager mViewPager; private QMUIContinuousNestedBottomRecyclerView mCurrentItemView; private int mCurrentPosition = -1; private IQMUIContinuousNestedBottomView.OnScrollNotifier mOnScrollNotifier; public QDContinuousBottomView(Context context) { super(context); } public QDContinuousBottomView(Context context, AttributeSet attrs) { super(context, attrs); } public QDContinuousBottomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @NonNull @Override protected View onCreateHeaderView() { TextView headerView = new TextView(getContext()); headerView.setTextSize(16); headerView.setTextColor(Color.BLACK); headerView.setBackgroundColor(Color.LTGRAY); headerView.setGravity(Gravity.CENTER); headerView.setText("This is normal view with ViewPager below"); return headerView; } @Override protected int getHeaderHeightLayoutParam() { return QMUIDisplayHelper.dp2px(getContext(), 200); } @Override protected int getHeaderStickyHeight() { return QMUIDisplayHelper.dp2px(getContext(), 50); } @NonNull @Override protected View onCreateContentView() { mViewPager = new MyViewPager(getContext()); mViewPager.setAdapter(new QMUIPagerAdapter() { @Override protected Object hydrate(ViewGroup container, int position) { QMUIContinuousNestedBottomRecyclerView recyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } }); BaseRecyclerAdapter adapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; adapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); recyclerView.setAdapter(adapter); onDataLoaded(adapter); return recyclerView; } @Override protected void populate(ViewGroup container, Object item, int position) { container.addView((View) item); } @Override protected void destroy(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public int getCount() { return 3; } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { return view == o; } @Override public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { super.setPrimaryItem(container, position, object); mCurrentItemView = (QMUIContinuousNestedBottomRecyclerView) object; mCurrentPosition = position; if (mOnScrollNotifier != null) { mCurrentItemView.injectScrollNotifier(mOnScrollNotifier); } } }); return mViewPager; } private void onDataLoaded(BaseRecyclerAdapter adapter) { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); adapter.setData(data); } class MyViewPager extends QMUIViewPager implements IQMUIContinuousNestedBottomView { static final String KEY_CURRENT_POSITION = "demo_bottom_current_position"; public MyViewPager(Context context) { super(context); } @Override public void consumeScroll(int dyUnconsumed) { if (mCurrentItemView != null) { mCurrentItemView.consumeScroll(dyUnconsumed); } } @Override public void smoothScrollYBy(int dy, int duration) { if (mCurrentItemView != null) { mCurrentItemView.smoothScrollYBy(dy, duration); } } @Override public void stopScroll() { if (mCurrentItemView != null) { mCurrentItemView.stopScroll(); } } @Override public int getContentHeight() { if (mCurrentItemView != null) { return mCurrentItemView.getContentHeight(); } return 0; } @Override public int getCurrentScroll() { if (mCurrentItemView != null) { return mCurrentItemView.getCurrentScroll(); } return 0; } @Override public int getScrollOffsetRange() { if (mCurrentItemView != null) { return mCurrentItemView.getScrollOffsetRange(); } return getHeight(); } @Override public void injectScrollNotifier(OnScrollNotifier notifier) { mOnScrollNotifier = notifier; if (mCurrentItemView != null) { mCurrentItemView.injectScrollNotifier(notifier); } } @Override public void saveScrollInfo(@NonNull Bundle bundle) { bundle.putInt(KEY_CURRENT_POSITION, mCurrentPosition); if(mCurrentItemView != null){ mCurrentItemView.saveScrollInfo(bundle); } } @Override public void restoreScrollInfo(@NonNull Bundle bundle) { if(mCurrentItemView != null){ int currentPos = bundle.getInt(KEY_CURRENT_POSITION, -1); if(currentPos == mCurrentPosition){ mCurrentItemView.restoreScrollInfo(bundle); } } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll1Fragment.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.fragment.lab; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.arch.record.RecordArgumentEditor; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopWebView; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "webview + recyclerview") @LatestVisitRecord public class QDContinuousNestedScroll1Fragment extends QDContinuousNestedScrollBaseFragment { private QMUIWebView mNestedWebView; private RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle arguments = getArguments(); if (arguments != null && arguments.getInt("fragment_test") == 20) { Toast.makeText(getContext(), "恢复到最近阅读(Int)", Toast.LENGTH_SHORT).show(); } } @Override protected void initCoordinatorLayout() { mNestedWebView = new QMUIContinuousNestedTopWebView(getContext()); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams webViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); webViewLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mNestedWebView, webViewLp); mRecyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp); mNestedWebView.loadUrl("https://mp.weixin.qq.com/s/zgfLOMD2JfZJKfHx-5BsBg"); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } @Override public void onCollectLatestVisitArgument(RecordArgumentEditor editor) { editor.putInt("fragment_test", 20); } @Override public void onDestroy() { super.onDestroy(); if (mNestedWebView != null) { mCoordinatorLayout.removeView(mNestedWebView); mNestedWebView.destroy(); mNestedWebView = null; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll2Fragment.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.fragment.lab; import android.util.Log; import android.view.ViewGroup; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopWebView; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import androidx.coordinatorlayout.widget.CoordinatorLayout; @Widget(group = Group.Other, name = "webview + part sticky header + viewpager") public class QDContinuousNestedScroll2Fragment extends QDContinuousNestedScrollBaseFragment { private static final String TAG = "ContinuousNestedScroll"; private QMUIWebView mNestedWebView; private QDContinuousBottomView mBottomView; @Override protected void initCoordinatorLayout() { mNestedWebView = new QMUIContinuousNestedTopWebView(getContext()); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams webViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); webViewLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mNestedWebView, webViewLp); mBottomView = new QDContinuousBottomView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mBottomView, recyclerViewLp); mNestedWebView.loadUrl("https://mp.weixin.qq.com/s/zgfLOMD2JfZJKfHx-5BsBg"); mCoordinatorLayout.addOnScrollListener(new QMUIContinuousNestedScrollLayout.OnScrollListener() { @Override public void onScroll(QMUIContinuousNestedScrollLayout scrollLayout, int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange) { Log.i(TAG, String.format("topCurrent = %d; topRange = %d; " + "offsetCurrent = %d; offsetRange = %d; " + "bottomCurrent = %d, bottomRange = %d", topCurrent, topRange, offsetCurrent, offsetRange, bottomCurrent, bottomRange)); } @Override public void onScrollStateChange(QMUIContinuousNestedScrollLayout scrollLayout, int newScrollState, boolean fromTopBehavior) { } }); } @Override public void onDestroy() { super.onDestroy(); if (mNestedWebView != null) { mCoordinatorLayout.removeView(mNestedWebView); mNestedWebView.destroy(); mNestedWebView = null; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll3Fragment.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.fragment.lab; import android.graphics.Color; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopRecyclerView; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "recyclerview + recyclerview") public class QDContinuousNestedScroll3Fragment extends QDContinuousNestedScrollBaseFragment { private QMUIContinuousNestedTopRecyclerView mTopRecyclerView; private RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; @Override protected void initCoordinatorLayout() { mTopRecyclerView = new QMUIContinuousNestedTopRecyclerView(getContext()); mTopRecyclerView.setBackgroundColor(Color.LTGRAY); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams webViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); webViewLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopRecyclerView, webViewLp); mRecyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp); mTopRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mTopRecyclerView.setAdapter(mAdapter); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll4Fragment.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.fragment.lab; import android.graphics.Color; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopDelegateLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopRecyclerView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.appcompat.widget.AppCompatTextView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "(header + recyclerview + bottom) + recyclerview") public class QDContinuousNestedScroll4Fragment extends QDContinuousNestedScrollBaseFragment { private QMUIContinuousNestedTopDelegateLayout mTopDelegateLayout; private QMUIContinuousNestedTopRecyclerView mTopRecyclerView; private RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; @Override protected void initCoordinatorLayout() { mTopDelegateLayout = new QMUIContinuousNestedTopDelegateLayout(getContext()); mTopDelegateLayout.setBackgroundColor(Color.LTGRAY); mTopRecyclerView = new QMUIContinuousNestedTopRecyclerView(getContext()); AppCompatTextView headerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; headerView.setTextSize(17); headerView.setBackgroundColor(Color.GRAY); headerView.setTextColor(Color.WHITE); headerView.setText("This is Top Header"); headerView.setGravity(Gravity.CENTER); mTopDelegateLayout.setHeaderView(headerView); AppCompatTextView footerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; footerView.setTextSize(17); footerView.setBackgroundColor(Color.GRAY); footerView.setTextColor(Color.WHITE); footerView.setGravity(Gravity.CENTER); footerView.setText("This is Top Footer"); mTopDelegateLayout.setFooterView(footerView); mTopDelegateLayout.setDelegateView(mTopRecyclerView); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams topLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); topLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopDelegateLayout, topLp); mRecyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp); mTopRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mTopRecyclerView.setAdapter(mAdapter); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll5Fragment.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.fragment.lab; import android.graphics.Color; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopDelegateLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopWebView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.appcompat.widget.AppCompatTextView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "(header + webview + bottom) + recyclerview") public class QDContinuousNestedScroll5Fragment extends QDContinuousNestedScrollBaseFragment { private QMUIContinuousNestedTopDelegateLayout mTopDelegateLayout; private QMUIContinuousNestedTopWebView mTopWebView; private RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; @Override protected void initCoordinatorLayout() { mTopDelegateLayout = new QMUIContinuousNestedTopDelegateLayout(getContext()); mTopDelegateLayout.setBackgroundColor(Color.LTGRAY); mTopWebView = new QMUIContinuousNestedTopWebView(getContext()); AppCompatTextView headerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; headerView.setTextSize(17); headerView.setBackgroundColor(Color.GRAY); headerView.setTextColor(Color.WHITE); headerView.setText("This is Top Header"); headerView.setGravity(Gravity.CENTER); mTopDelegateLayout.setHeaderView(headerView); final AppCompatTextView footerView = new AppCompatTextView(getContext()); footerView.setTextSize(17); footerView.setBackgroundColor(Color.GRAY); footerView.setTextColor(Color.WHITE); footerView.setGravity(Gravity.CENTER); footerView.setText("点击展开更多\nThis is Top Footer\nThis is Top Footer\nThis is Top Footer\n"); footerView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CharSequence text = footerView.getText(); footerView.setText("" + text + text); } }); mTopDelegateLayout.setFooterView(footerView); mTopDelegateLayout.setDelegateView(mTopWebView); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams topLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); topLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopDelegateLayout, topLp); mRecyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mTopWebView.loadUrl("https://mp.weixin.qq.com/s/zgfLOMD2JfZJKfHx-5BsBg"); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } @Override public void onDestroy() { super.onDestroy(); if (mTopWebView != null) { mCoordinatorLayout.removeView(mTopWebView); mTopWebView.destroy(); mTopWebView = null; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll6Fragment.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.fragment.lab; import android.graphics.Color; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopLinearLayout; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.appcompat.widget.AppCompatTextView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "linearLayout + recyclerview") public class QDContinuousNestedScroll6Fragment extends QDContinuousNestedScrollBaseFragment { private QMUIContinuousNestedTopLinearLayout mTopLinearLayout; private RecyclerView mRecyclerView; private BaseRecyclerAdapter mAdapter; @Override protected void initCoordinatorLayout() { mTopLinearLayout = new QMUIContinuousNestedTopLinearLayout(getContext()); mTopLinearLayout.setBackgroundColor(Color.LTGRAY); mTopLinearLayout.setOrientation(LinearLayout.VERTICAL); AppCompatTextView firstView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 1000), View.MeasureSpec.EXACTLY )); } }; firstView.setTextSize(17); firstView.setBackgroundColor(Color.DKGRAY); firstView.setTextColor(Color.WHITE); firstView.setText("This is Top firstView"); firstView.setGravity(Gravity.CENTER); mTopLinearLayout.addView(firstView); AppCompatTextView secondView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 1000), MeasureSpec.EXACTLY )); } }; secondView.setTextSize(17); secondView.setBackgroundColor(Color.GRAY); secondView.setTextColor(Color.WHITE); secondView.setGravity(Gravity.CENTER); secondView.setText("This is secondView"); mTopLinearLayout.addView(secondView); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams topLp = new CoordinatorLayout.LayoutParams( matchParent, ViewGroup.LayoutParams.WRAP_CONTENT); topLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopLinearLayout, topLp); mRecyclerView = new QMUIContinuousNestedBottomRecyclerView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll7Fragment.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.fragment.lab; import android.graphics.Color; import android.util.Log; import android.view.Gravity; import android.view.ViewGroup; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopDelegateLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopWebView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import androidx.appcompat.widget.AppCompatTextView; import androidx.coordinatorlayout.widget.CoordinatorLayout; @Widget(group = Group.Other, name = "(header + webview + bottom) + (part sticky header + viewpager)") public class QDContinuousNestedScroll7Fragment extends QDContinuousNestedScrollBaseFragment { private static final String TAG = "ContinuousNestedScroll"; private QMUIContinuousNestedTopDelegateLayout mTopDelegateLayout; private QMUIContinuousNestedTopWebView mNestedWebView; private QDContinuousBottomView mBottomView; @Override protected void initCoordinatorLayout() { mTopDelegateLayout = new QMUIContinuousNestedTopDelegateLayout(getContext()); mTopDelegateLayout.setBackgroundColor(Color.LTGRAY); mNestedWebView = new QMUIContinuousNestedTopWebView(getContext()); AppCompatTextView headerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; headerView.setTextSize(17); headerView.setBackgroundColor(Color.GRAY); headerView.setTextColor(Color.WHITE); headerView.setText("This is Top Header"); headerView.setGravity(Gravity.CENTER); mTopDelegateLayout.setHeaderView(headerView); AppCompatTextView footerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; footerView.setTextSize(17); footerView.setBackgroundColor(Color.GRAY); footerView.setTextColor(Color.WHITE); footerView.setGravity(Gravity.CENTER); footerView.setText("This is Top Footer"); mTopDelegateLayout.setFooterView(footerView); mTopDelegateLayout.setDelegateView(mNestedWebView); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams topLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); topLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopDelegateLayout, topLp); mBottomView = new QDContinuousBottomView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mBottomView, recyclerViewLp); mNestedWebView.loadUrl("https://mp.weixin.qq.com/s/zgfLOMD2JfZJKfHx-5BsBg"); mCoordinatorLayout.addOnScrollListener(new QMUIContinuousNestedScrollLayout.OnScrollListener() { @Override public void onScroll(QMUIContinuousNestedScrollLayout scrollLayout, int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange) { Log.i(TAG, String.format("topCurrent = %d; topRange = %d; " + "offsetCurrent = %d; offsetRange = %d; " + "bottomCurrent = %d, bottomRange = %d", topCurrent, topRange, offsetCurrent, offsetRange, bottomCurrent, bottomRange)); } @Override public void onScrollStateChange(QMUIContinuousNestedScrollLayout scrollLayout, int newScrollState, boolean fromTopBehavior) { Log.i(TAG, String.format("newScrollState = %d; fromTopBehavior = %b", newScrollState, fromTopBehavior)); } }); } @Override public void onDestroy() { super.onDestroy(); if (mNestedWebView != null) { mCoordinatorLayout.removeView(mNestedWebView); mNestedWebView.destroy(); mNestedWebView = null; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScroll8Fragment.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.fragment.lab; import android.graphics.Color; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopDelegateLayout; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopRecyclerView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmuidemo.base.BaseRecyclerAdapter; import com.qmuiteam.qmuidemo.base.RecyclerViewHolder; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.appcompat.widget.AppCompatTextView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @Widget(group = Group.Other, name = "(header + recyclerView + bottom) + (part sticky header + viewpager)") public class QDContinuousNestedScroll8Fragment extends QDContinuousNestedScrollBaseFragment { private static final String TAG = "ContinuousNestedScroll"; private QMUIContinuousNestedTopDelegateLayout mTopDelegateLayout; private QMUIContinuousNestedTopRecyclerView mTopRecyclerView; private QDContinuousBottomView mBottomView; private BaseRecyclerAdapter mAdapter; @Override protected void initCoordinatorLayout() { mTopDelegateLayout = new QMUIContinuousNestedTopDelegateLayout(getContext()); mTopDelegateLayout.setBackgroundColor(Color.LTGRAY); new QMUIContinuousNestedTopRecyclerView(getContext()); AppCompatTextView headerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; headerView.setTextSize(17); headerView.setBackgroundColor(Color.GRAY); headerView.setTextColor(Color.WHITE); headerView.setText("This is Top Header"); headerView.setGravity(Gravity.CENTER); mTopDelegateLayout.setHeaderView(headerView); AppCompatTextView footerView = new AppCompatTextView(getContext()) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 100), MeasureSpec.EXACTLY )); } }; footerView.setTextSize(17); footerView.setBackgroundColor(Color.GRAY); footerView.setTextColor(Color.WHITE); footerView.setGravity(Gravity.CENTER); footerView.setText("This is Top Footer"); mTopDelegateLayout.setFooterView(footerView); mTopRecyclerView = new QMUIContinuousNestedTopRecyclerView(getContext()); mTopRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()) { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }); mTopDelegateLayout.setDelegateView(mTopRecyclerView); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; CoordinatorLayout.LayoutParams topLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); topLp.setBehavior(new QMUIContinuousNestedTopAreaBehavior(getContext())); mCoordinatorLayout.setTopAreaView(mTopDelegateLayout, topLp); mBottomView = new QDContinuousBottomView(getContext()); CoordinatorLayout.LayoutParams recyclerViewLp = new CoordinatorLayout.LayoutParams( matchParent, matchParent); recyclerViewLp.setBehavior(new QMUIContinuousNestedBottomAreaBehavior()); mCoordinatorLayout.setBottomAreaView(mBottomView, recyclerViewLp); mCoordinatorLayout.addOnScrollListener(new QMUIContinuousNestedScrollLayout.OnScrollListener() { @Override public void onScroll(QMUIContinuousNestedScrollLayout scrollLayout, int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange) { Log.i(TAG, String.format("topCurrent = %d; topRange = %d; " + "offsetCurrent = %d; offsetRange = %d; " + "bottomCurrent = %d, bottomRange = %d", topCurrent, topRange, offsetCurrent, offsetRange, bottomCurrent, bottomRange)); } @Override public void onScrollStateChange(QMUIContinuousNestedScrollLayout scrollLayout, int newScrollState, boolean fromTopBehavior) { } }); mAdapter = new BaseRecyclerAdapter(getContext(), null) { @Override public int getItemLayoutId(int viewType) { return android.R.layout.simple_list_item_1; } @Override public void bindData(RecyclerViewHolder holder, int position, String item) { holder.setText(android.R.id.text1, item); } }; mAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { @Override public void onItemClick(View itemView, int pos) { Toast.makeText(getContext(), "click position=" + pos, Toast.LENGTH_SHORT).show(); } }); mTopRecyclerView.setAdapter(mAdapter); onDataLoaded(); } private void onDataLoaded() { List data = new ArrayList<>(Arrays.asList("Helps", "Maintain", "Liver", "Health", "Function", "Supports", "Healthy", "Fat", "Metabolism", "Nuturally", "Bracket", "Refrigerator", "Bathtub", "Wardrobe", "Comb", "Apron", "Carpet", "Bolster", "Pillow", "Cushion")); Collections.shuffle(data); mAdapter.setData(data); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScrollBaseFragment.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.fragment.lab; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; public abstract class QDContinuousNestedScrollBaseFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBarLayout; @BindView(R.id.pull_to_refresh) QMUIPullRefreshLayout mPullRefreshLayout; @BindView(R.id.coordinator) QMUIContinuousNestedScrollLayout mCoordinatorLayout; private Bundle mSavedScrollInfo = new Bundle(); @Override protected View onCreateView() { View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_continuous_nested_scroll, null); ButterKnife.bind(this, view); initTopBar(); initPullRefreshLayout(); initCoordinatorLayout(); mCoordinatorLayout.setDraggableScrollBarEnabled(true); return view; } private void initPullRefreshLayout(){ mPullRefreshLayout.setOnPullListener(new QMUIPullRefreshLayout.OnPullListener() { @Override public void onMoveTarget(int offset) { } @Override public void onMoveRefreshView(int offset) { } @Override public void onRefresh() { mPullRefreshLayout.postDelayed(new Runnable() { @Override public void run() { mPullRefreshLayout.finishRefresh(); } }, 3000); } }); } private void initTopBar() { mTopBarLayout.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBarLayout.setTitle(QDDataManager.getInstance().getName(this.getClass())); mTopBarLayout.addRightTextButton("scroll", QMUIViewHelper.generateViewId()) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBottomSheet(); } }); } protected abstract void initCoordinatorLayout(); private void showBottomSheet() { new QMUIBottomSheet.BottomListSheetBuilder(getContext()) .addItem("scrollToBottom") .addItem("scrollToTop") .addItem("scrollBottomViewToTop") .addItem("scrollBy 40dp") .addItem("scrollBy -40dp") .addItem("smoothScrollBy 100dp/1s") .addItem("smoothScrollBy -100dp/1s") .addItem("save current scroll info") .addItem("restore scroll info") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { switch (position) { case 0: mCoordinatorLayout.scrollToBottom(); break; case 1: mCoordinatorLayout.scrollToTop(); break; case 2: mCoordinatorLayout.scrollBottomViewToTop(); break; case 3: mCoordinatorLayout.scrollBy(QMUIDisplayHelper.dp2px(getContext(), 40)); break; case 4: mCoordinatorLayout.scrollBy(QMUIDisplayHelper.dp2px(getContext(), -40)); break; case 5: mCoordinatorLayout.smoothScrollBy(QMUIDisplayHelper.dp2px(getContext(), 100), 1000); break; case 6: mCoordinatorLayout.smoothScrollBy(QMUIDisplayHelper.dp2px(getContext(), -100), 1000); break; case 7: mCoordinatorLayout.saveScrollInfo(mSavedScrollInfo); break; case 8: mCoordinatorLayout.restoreScrollInfo(mSavedScrollInfo); } dialog.dismiss(); } }) .build().show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDContinuousNestedScrollFragment.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.fragment.lab; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedScrollLayout; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Lab, widgetClass = QMUIContinuousNestedScrollLayout.class, iconRes = R.mipmap.icon_grid_continuous_nest_scroll, docUrl ="https://github.com/Tencent/QMUI_Android/wiki/QMUIContinuousNestedScrollLayout") public class QDContinuousNestedScrollFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mQDDataManager = QDDataManager.getInstance(); } @Override protected View onCreateView() { View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, view); initTopBar(); initGroupListView(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDDataManager.getName(this.getClass())); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll1Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll1Fragment fragment = new QDContinuousNestedScroll1Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll2Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll2Fragment fragment = new QDContinuousNestedScroll2Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll3Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll3Fragment fragment = new QDContinuousNestedScroll3Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll4Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll4Fragment fragment = new QDContinuousNestedScroll4Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll5Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll5Fragment fragment = new QDContinuousNestedScroll5Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll6Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll6Fragment fragment = new QDContinuousNestedScroll6Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll7Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll7Fragment fragment = new QDContinuousNestedScroll7Fragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDContinuousNestedScroll8Fragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDContinuousNestedScroll8Fragment fragment = new QDContinuousNestedScroll8Fragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEditorFragment.kt ================================================ package com.qmuiteam.qmuidemo.fragment.lab import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.qmuiteam.compose.core.ui.QMUITopBar import com.qmuiteam.compose.core.ui.QMUITopBarBackIconItem import com.qmuiteam.editor.* import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.ComposeBaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch @Widget(name = "QMUI Editor", iconRes = R.mipmap.icon_grid_in_progress) @LatestVisitRecord class QDEditorFragment : ComposeBaseFragment() { @Composable fun TextButton(text: String ,onClick: ()-> Unit){ Text(text, modifier = Modifier .clickable { onClick() } .padding(8.dp)) } @Composable fun QDEditor() { val channel = remember { Channel() } val scope = rememberCoroutineScope() Column(modifier = Modifier.fillMaxSize()) { QMUIEditor( modifier = Modifier .fillMaxWidth() .weight(1f) .padding(16.dp), value = TextFieldValue(""), hint = AnnotatedString("写下这一刻的想法"), channel = channel ) { } Row(modifier = Modifier .fillMaxWidth() .height(60.dp)) { TextButton("加粗"){ scope.launch { channel.send(BoldBehavior(500)) } } TextButton("引用"){ scope.launch { channel.send(QuoteBehavior) } } TextButton("无序列表"){ scope.launch { channel.send(UnOrderListBehavior) } } TextButton("Header"){ scope.launch { channel.send(HeaderBehavior(HeaderLevel.h2)) } } } } } @Composable override fun PageContent() { Column(modifier = Modifier.fillMaxSize()) { QMUITopBar( title = "QMUIEditor", leftItems = arrayListOf( QMUITopBarBackIconItem { popBackStack() } ) ) Box(modifier = Modifier .fillMaxWidth() .weight(1f)) { QDEditor() } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDEmojiInputFragment.kt ================================================ package com.qmuiteam.qmuidemo.fragment.lab import android.content.Context import android.text.Editable import android.text.TextWatcher import android.util.Log import android.view.Gravity import android.view.View import android.widget.EditText import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatEditText import androidx.constraintlayout.widget.ConstraintLayout import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmui.kotlin.* import com.qmuiteam.qmui.type.parser.EmojiTextParser import com.qmuiteam.qmui.type.view.EmojiEditText import com.qmuiteam.qmui.widget.QMUITopBarLayout import com.qmuiteam.qmuidemo.QDQQFaceManager import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.BaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget @Widget(name = "EmojiEditText", iconRes = R.mipmap.icon_grid_in_progress) @LatestVisitRecord class QDEmojiInputFragment : BaseFragment() { override fun onCreateView(): View { return EmojiLayout(requireContext()) } } class EmojiLayout(context: Context): ConstraintLayout(context){ val topBarLayout = QMUITopBarLayout(context).apply { fitsSystemWindows = true id = View.generateViewId() } val editText = EmojiEditText(context).apply { gravity = Gravity.TOP or Gravity.LEFT textParser = EmojiTextParser(QDQQFaceManager.getInstance()) { true } } val se = TextView(context).apply { text = "[色]" setPadding(0, dip(20), 0, dip(20)) onClick { editText.replaceSelection("[色]") } } val weixiao = TextView(context).apply { text = "[微笑]" setPadding(0, dip(20), 0, dip(20)) onClick { editText.replaceSelection("[微笑]") } } val daku = TextView(context).apply { text = "[大哭]" setPadding(0, dip(20), 0, dip(20)) onClick { editText.replaceSelection("[大哭]") } } val delete = TextView(context).apply { text = "delete" setPadding(0, dip(20), 0, dip(20)) onClick { editText.delete() } } val toolBar = LinearLayout(context).apply { id = View.generateViewId() orientation = LinearLayout.HORIZONTAL addView(se, LinearLayout.LayoutParams(0, wrapContent, 1f)) addView(weixiao, LinearLayout.LayoutParams(0, wrapContent, 1f)) addView(daku, LinearLayout.LayoutParams(0, wrapContent, 1f)) addView(delete, LinearLayout.LayoutParams(0, wrapContent, 1f)) } init { addView(topBarLayout, LayoutParams(0, wrapContent).apply { alignParentHor() topToTop = constraintParentId }) addView(toolBar, LayoutParams(0, wrapContent).apply { alignParentHor() bottomToBottom = constraintParentId }) addView(editText, LayoutParams(0, 0).apply { alignParentHor() topToBottom = topBarLayout.id bottomToTop = toolBar.id }) editText.setText("反反复复[微笑][色]发发发方法") } fun handleClick(text: String){ val origin = editText.text if(origin == null){ editText.setText(text) }else{ origin.append(text) } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoClipFragment.kt ================================================ package com.qmuiteam.qmuidemo.fragment.lab import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri import com.qmuiteam.photo.coil.QMUICoilPhotoProvider import com.qmuiteam.photo.compose.QMUIPhotoClipper import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.ComposeBaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget @Widget(name = "QMUI Photo Clip", iconRes = R.mipmap.icon_grid_in_progress) @LatestVisitRecord class QDPhotoClipFragment : ComposeBaseFragment() { @Composable override fun PageContent() { var ret by remember { mutableStateOf(null) } QMUIPhotoClipper( photoProvider = QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 0f ) ) { doClip -> Row( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) ) { Box(modifier = Modifier .weight(1f) .clickable { popBackStack() } .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { Text( "取消", fontSize = 20.sp, color = Color.White, fontWeight = FontWeight.Bold ) } Box(modifier = Modifier .weight(1f) .clickable { ret = doClip() } .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { Text( "确定", fontSize = 20.sp, color = Color.White, fontWeight = FontWeight.Bold ) } } ret?.let { Image(bitmap = it.asImageBitmap(), contentDescription = "") } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt ================================================ package com.qmuiteam.qmuidemo.fragment.lab import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import com.qmuiteam.compose.core.ui.QMUITopBarBackIconItem import com.qmuiteam.compose.core.ui.QMUITopBarTextItem import com.qmuiteam.compose.core.ui.QMUITopBarWithLazyScrollState import com.qmuiteam.photo.activity.QMUIPhotoPickResult import com.qmuiteam.photo.activity.QMUIPhotoPickerActivity import com.qmuiteam.photo.activity.getQMUIPhotoPickResult import com.qmuiteam.photo.coil.QMUICoilPhotoProvider import com.qmuiteam.photo.coil.QMUIMediaCoilPhotoProviderFactory import com.qmuiteam.photo.compose.QMUIPhotoThumbnailWithViewer import com.qmuiteam.photo.util.QMUIPhotoHelper import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord import com.qmuiteam.qmuidemo.R import com.qmuiteam.qmuidemo.base.ComposeBaseFragment import com.qmuiteam.qmuidemo.lib.annotation.Widget import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Widget(name = "QMUI Photo", iconRes = R.mipmap.icon_grid_in_progress) @LatestVisitRecord class QDPhotoFragment : ComposeBaseFragment() { val pickerFlow = MutableStateFlow(null) private val pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { val pickerResult = it.data?.getQMUIPhotoPickResult() ?: return@registerForActivityResult pickerFlow.value = pickerResult } } @Composable override fun PageContent() { Column(modifier = Modifier.fillMaxSize()) { val scrollState = rememberLazyListState() QMUITopBarWithLazyScrollState( scrollState = scrollState, title = "QMUIPhoto", leftItems = arrayListOf( QMUITopBarBackIconItem { popBackStack() } ), rightItems = arrayListOf( QMUITopBarTextItem("Pick a Picture") { val activity = activity ?: return@QMUITopBarTextItem pickLauncher.launch( QMUIPhotoPickerActivity.intentOf( activity, QMUIPhotoPickerActivity::class.java, QMUIMediaCoilPhotoProviderFactory::class.java ) ) } ) ) LazyColumn( state = scrollState, modifier = Modifier .fillMaxWidth() .weight(1f) .background(Color.White), contentPadding = PaddingValues(start = 44.dp) ) { item { PickerResult() } // item { // TestImageCompress() // } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 1f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "file:///android_asset/test.png".toUri(), 0.0125f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 1f ) ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 1f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), ) ) } } item { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = listOf( QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 1f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png".toUri(), 1.379f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg".toUri(), 0.749f ), QMUICoilPhotoProvider( "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg".toUri(), 1f ), ) ) } } } } } @Composable fun PickerResult() { val pickResultState = pickerFlow.collectAsState() val pickResult = pickResultState.value if (pickResult == null) { Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) .clickable { val activity = activity ?: return@clickable pickLauncher.launch( QMUIPhotoPickerActivity.intentOf( activity, QMUIPhotoPickerActivity::class.java, QMUIMediaCoilPhotoProviderFactory::class.java ) ) } ) { Text("No Picked Images, click to pick") } } else { val images = remember(pickResult) { pickResult.list.map { QMUICoilPhotoProvider( it.uri, it.ratio() ) } } Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 20.dp) ) { Text(text = "原图:${pickResult.isOriginOpen}") QMUIPhotoThumbnailWithViewer( activity = requireActivity(), images = images ) } } } @Composable fun TestImageCompress() { var bitmap by remember { mutableStateOf(null) } LaunchedEffect("") { lifecycleScope.launch { withContext(Dispatchers.IO) { QMUIPhotoHelper.compressByShortEdgeWidthAndByteSize( requireContext(), { it.assets.open("test.png") }, 500 )?.inputStream().use { if (it != null) { bitmap = BitmapFactory.decodeStream(it) } } } } } if (bitmap != null) { Image(painter = BitmapPainter(bitmap!!.asImageBitmap()), contentDescription = "") } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDSchemeFragment.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.fragment.lab; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.manager.QDSchemeManager; import butterknife.BindView; import butterknife.ButterKnife; @LatestVisitRecord @Widget(group = Group.Lab, name = "Scheme", iconRes = R.mipmap.icon_grid_in_progress) public class QDSchemeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.edit_text) EditText mEditText; @BindView(R.id.button) QMUIRoundButton mBtn; @Override protected View onCreateView() { View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_scheme, null); ButterKnife.bind(this, view); initTopBar(); initEditStuff(); return view; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getDescription(this.getClass()).getName()); } private void initEditStuff() { mBtn.setEnabled(false); mEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (s == null || s.length() == 0) { mBtn.setEnabled(false); } mBtn.setEnabled(true); } }); mBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QDSchemeManager.getInstance().handle(mEditText.getText().toString()); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDSnapHelperFragment.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.fragment.lab; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.PagerSnapHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SnapHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIBottomSheet; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.adaptor.QDRecyclerViewAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.model.QDItemDescription; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import butterknife.BindView; import butterknife.ButterKnife; /** 使用 {@link SnapHelper} 实现 {@link RecyclerView} 按页滚动。 * Created by cgspine on 15/9/15. */ @Widget(group = Group.Lab, name = "用SnapHelper实现RecyclerView按页滚动", iconRes = R.mipmap.icon_grid_pager_layout_manager) public class QDSnapHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.pagerWrap) ViewGroup mPagerWrap; RecyclerView mRecyclerView; LinearLayoutManager mPagerLayoutManager; QDRecyclerViewAdapter mRecyclerViewAdapter; SnapHelper mSnapHelper; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getContext()).inflate(R.layout.fragment_pagerlayoutmanager, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); mRecyclerView = new RecyclerView(getContext()); mPagerLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false); mRecyclerView.setLayoutManager(mPagerLayoutManager); mRecyclerViewAdapter = new QDRecyclerViewAdapter(); mRecyclerViewAdapter.setItemCount(10); mRecyclerView.setAdapter(mRecyclerViewAdapter); mPagerWrap.addView(mRecyclerView); // PagerSnapHelper每次只能滚动一个item;用LinearSnapHelper则可以一次滚动多个,并最终保证定位 // mSnapHelper = new LinearSnapHelper(); mSnapHelper = new PagerSnapHelper(); mSnapHelper.attachToRecyclerView(mRecyclerView); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); // 切换其他情况的按钮 mTopBar.addRightImageButton(R.mipmap.icon_topbar_overflow, R.id.topbar_right_change_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showBottomSheetList(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void showBottomSheetList() { new QMUIBottomSheet.BottomListSheetBuilder(getActivity()) .addItem("水平方向") .addItem("垂直方向") .setOnSheetItemClickListener(new QMUIBottomSheet.BottomListSheetBuilder.OnSheetItemClickListener() { @Override public void onClick(QMUIBottomSheet dialog, View itemView, int position, String tag) { dialog.dismiss(); switch (position) { case 0: mPagerLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); break; case 1: mPagerLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); break; default: break; } } }) .build() .show(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDSwipeDeleteListViewFragment.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.fragment.lab; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import androidx.collection.LongSparseArray; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ListView; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.adaptor.QDSimpleAdapter; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; /** * @author cginechen * @date 2017-03-29 */ @Widget(group = Group.Lab, name = "ListView滑动删除") public class QDSwipeDeleteListViewFragment extends BaseFragment { private static final int SWIPE_DURATION = 250; private static final int MOVE_DURATION = 150; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.listview) ListView mListView; LongSparseArray mItemIdTopMap = new LongSparseArray<>(); private MyAdapter mAdapter; boolean mSwiping = false; boolean mItemPressed = false; @Override protected View onCreateView() { View root = LayoutInflater.from(getContext()).inflate(R.layout.fragment_swipe_delete_listview, null); ButterKnife.bind(this, root); initTopBar(); initListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initListView() { List data = new ArrayList<>(); for (int i = 0; i < 20; i++) { data.add("item " + (i + 1)); } mAdapter = new MyAdapter(getContext(), data, new View.OnTouchListener() { float mDownX; private int mSwipeSlop = -1; @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public boolean onTouch(final View v, MotionEvent event) { if (mSwipeSlop < 0) { mSwipeSlop = ViewConfiguration.get(QDSwipeDeleteListViewFragment.this.getContext()). getScaledTouchSlop(); } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (mItemPressed) { return false; } mItemPressed = true; mDownX = event.getX(); break; case MotionEvent.ACTION_CANCEL: v.setAlpha(1); v.setTranslationX(0); mItemPressed = false; break; case MotionEvent.ACTION_MOVE: { float x = event.getX() + v.getTranslationX(); float deltaX = x - mDownX; float deltaXAbs = Math.abs(deltaX); if (!mSwiping) { if (deltaXAbs > mSwipeSlop) { mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); } } if (mSwiping) { v.setTranslationX((x - mDownX)); v.setAlpha(1 - deltaXAbs / v.getWidth()); } } break; case MotionEvent.ACTION_UP: { if (mSwiping) { float x = event.getX() + v.getTranslationX(); float deltaX = x - mDownX; float deltaXAbs = Math.abs(deltaX); float fractionCovered; float endX; float endAlpha; final boolean remove; if (deltaXAbs > v.getWidth() / 4) { // Greater than a quarter of the width - animate it out fractionCovered = deltaXAbs / v.getWidth(); endX = deltaX < 0 ? -v.getWidth() : v.getWidth(); endAlpha = 0; remove = true; } else { // Not far enough - animate it back fractionCovered = 1 - (deltaXAbs / v.getWidth()); endX = 0; endAlpha = 1; remove = false; } // Animate position and alpha of swiped item // NOTE: This is a simplified version of swipe behavior, for the // purposes of this demo about animation. A real version should use // velocity (via the VelocityTracker class) to send the item off or // back at an appropriate speed. long duration = (int) ((1 - fractionCovered) * SWIPE_DURATION); mListView.setEnabled(false); v.animate().setDuration(duration). alpha(endAlpha).translationX(endX). withEndAction(new Runnable() { @Override public void run() { // Restore animated values v.setAlpha(1); v.setTranslationX(0); if (remove) { animateRemoval(mListView, v); } else { mSwiping = false; mListView.setEnabled(true); } } }); } } mItemPressed = false; break; default: return false; } return true; } }); mListView.setAdapter(mAdapter); } private void animateRemoval(final ListView listview, View viewToRemove) { int firstVisiblePosition = listview.getFirstVisiblePosition(); for (int i = 0; i < listview.getChildCount(); ++i) { View child = listview.getChildAt(i); if (child != viewToRemove) { int position = firstVisiblePosition + i; long itemId = mAdapter.getItemId(position); mItemIdTopMap.put(itemId, child.getTop()); } } // Delete the item from the adapter int position = mListView.getPositionForView(viewToRemove); mAdapter.remove(position); final ViewTreeObserver observer = listview.getViewTreeObserver(); observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public boolean onPreDraw() { observer.removeOnPreDrawListener(this); boolean firstAnimation = true; int firstVisiblePosition = listview.getFirstVisiblePosition(); for (int i = 0; i < listview.getChildCount(); ++i) { final View child = listview.getChildAt(i); int position = firstVisiblePosition + i; long itemId = mAdapter.getItemId(position); Integer startTop = mItemIdTopMap.get(itemId); int top = child.getTop(); if (startTop != null) { if (startTop != top) { int delta = startTop - top; child.setTranslationY(delta); child.animate().setDuration(MOVE_DURATION).translationY(0); if (firstAnimation) { child.animate().withEndAction(new Runnable() { public void run() { mSwiping = false; mListView.setEnabled(true); } }); firstAnimation = false; } } } else { // Animate new views along with the others. The catch is that they did not // exist in the start state, so we must calculate their starting position // based on neighboring views. int childHeight = child.getHeight() + listview.getDividerHeight(); startTop = top + (i > 0 ? childHeight : -childHeight); int delta = startTop - top; child.setTranslationY(delta); child.animate().setDuration(MOVE_DURATION).translationY(0); if (firstAnimation) { child.animate().withEndAction(new Runnable() { public void run() { mSwiping = false; mListView.setEnabled(true); } }); firstAnimation = false; } } } mItemIdTopMap.clear(); return true; } }); } private static class MyAdapter extends QDSimpleAdapter { private View.OnTouchListener mTouchListener; public MyAdapter(Context context, List data, View.OnTouchListener listener) { super(context, data); mTouchListener = listener; } @Override public long getItemId(int position) { return getItem(position).hashCode(); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); view.setOnTouchListener(mTouchListener); return view; } @Override public boolean hasStableIds() { return true; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewBridgeFragment.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.fragment.lab; import android.annotation.TargetApi; import android.os.Bundle; import android.view.View; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.widget.Toast; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction; import com.qmuiteam.qmui.widget.webview.QMUIBridgeWebViewClient; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmui.widget.webview.QMUIWebViewBridgeHandler; import com.qmuiteam.qmui.widget.webview.QMUIWebViewClient; import com.qmuiteam.qmui.widget.webview.QMUIWebViewContainer; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDSchemeManager; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; @Widget(group = Group.Other, name = "Webview Bridge") public class QDWebViewBridgeFragment extends QDWebExplorerFragment { public QDWebViewBridgeFragment() { String url = "file:///android_asset/demo.html"; Bundle bundle = new Bundle(); bundle.putString(EXTRA_URL, url); bundle.putString(EXTRA_TITLE, "测试 Bridge"); setArguments(bundle); } @Override protected boolean needDispatchSafeAreaInset() { return false; } @Override protected void configWebView(QMUIWebViewContainer webViewContainer, QMUIWebView webView) { webView.setCallback(new QMUIWebView.Callback() { @Override public void onSureNotSupportChangeCssEnv() { new QMUIDialog.MessageDialogBuilder(getContext()) .setMessage("Do not support to change css env") .addAction(new QMUIDialogAction(getContext(), R.string.ok, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { dialog.dismiss(); } })) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .show(); } }); } @Override protected WebChromeClient getWebViewChromeClient() { return new ExplorerWebViewChromeClient(this) { @Override public void onShowCustomView(View view, CustomViewCallback callback) { super.onShowCustomView(view, callback); mTopBarLayout.setBackgroundAlpha(0); } @Override public void onHideCustomView() { super.onHideCustomView(); } }; } @Override protected QMUIWebViewClient getWebViewClient() { QMUIWebViewBridgeHandler handler = new QMUIWebViewBridgeHandler(mWebView) { @Override protected List getSupportedCmdList() { List ret = new ArrayList<>(); ret.add("test"); return ret; } @Override protected void handleMessage(String message, MessageFinishCallback callback) { try { JSONObject json = new JSONObject(message); String id = json.getString("id"); String info = json.getString("info"); Toast.makeText(getContext(), "id = " + id + "; info = " + info, Toast.LENGTH_SHORT).show(); JSONObject result = new JSONObject(); result.put("code", 100); result.put("message", "Native 的执行结果"); callback.finish(result); } catch (JSONException e) { e.printStackTrace(); callback.finish(null); } } }; return new QMUIBridgeWebViewClient(needDispatchSafeAreaInset(), false, handler){ @Override @TargetApi(21) protected boolean onShouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if(QDSchemeManager.getInstance().handle(request.getUrl().toString())){ return true; } return super.onShouldOverrideUrlLoading(view, request); } @Override protected boolean onShouldOverrideUrlLoading(WebView view, String url) { if(QDSchemeManager.getInstance().handle(url)){ return true; } return super.onShouldOverrideUrlLoading(view, url); } }; } @Override protected void onScrollWebContent(int scrollX, int scrollY, int oldScrollX, int oldScrollY) { mTopBarLayout.computeAndSetBackgroundAlpha(scrollY, 0, QMUIDisplayHelper.dp2px(getContext(), 20)); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewFixFragment.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.fragment.lab; import android.os.Bundle; import android.view.View; import android.webkit.WebChromeClient; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.dialog.QMUIDialogAction; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmui.widget.webview.QMUIWebViewContainer; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.fragment.QDWebExplorerFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; @Widget(group = Group.Other, name = "修复 css-env-safe-area-inset") public class QDWebViewFixFragment extends QDWebExplorerFragment { public QDWebViewFixFragment() { String url = "http://cgsdream.org/static/html/test-css-env-safe-area-inset.html"; Bundle bundle = new Bundle(); bundle.putString(EXTRA_URL, url); bundle.putString(EXTRA_TITLE, "test-css-env-safe-area-inset"); setArguments(bundle); } @Override protected boolean needDispatchSafeAreaInset() { return true; } @Override protected void configWebView(QMUIWebViewContainer webViewContainer, QMUIWebView webView) { webView.setCallback(new QMUIWebView.Callback() { @Override public void onSureNotSupportChangeCssEnv() { new QMUIDialog.MessageDialogBuilder(getContext()) .setMessage("Do not support to change css env") .addAction(new QMUIDialogAction(getContext(), R.string.ok, new QMUIDialogAction.ActionListener() { @Override public void onClick(QMUIDialog dialog, int index) { dialog.dismiss(); } })) .setSkinManager(QMUISkinManager.defaultInstance(getContext())) .show(); } }); } @Override protected WebChromeClient getWebViewChromeClient() { return new ExplorerWebViewChromeClient(this) { @Override public void onShowCustomView(View view, CustomViewCallback callback) { super.onShowCustomView(view, callback); mTopBarLayout.setBackgroundAlpha(0); } @Override public void onHideCustomView() { super.onHideCustomView(); } }; } @Override protected void onScrollWebContent(int scrollX, int scrollY, int oldScrollX, int oldScrollY) { mTopBarLayout.computeAndSetBackgroundAlpha(scrollY, 0, QMUIDisplayHelper.dp2px(getContext(), 20)); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDWebViewFragment.java ================================================ package com.qmuiteam.qmuidemo.fragment.lab; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; @Widget(group = Group.Lab, widgetClass = QMUIWebView.class, iconRes = R.mipmap.icon_grid_webview) public class QDWebViewFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDDataManager mQDDataManager; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDDataManager = QDDataManager.getInstance(); mQDItemDescription = mQDDataManager.getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDWebViewFixFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDWebViewFixFragment fragment = new QDWebViewFixFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(mQDDataManager.getName( QDWebViewBridgeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDWebViewBridgeFragment fragment = new QDWebViewBridgeFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDColorHelperFragment.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.fragment.util; import androidx.core.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIColorHelper; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIColorHelper} 的使用示例。 * Created by Kayo on 2016/12/1. */ @Widget(group = Group.Helper, widgetClass = QMUIColorHelper.class, iconRes = R.mipmap.icon_grid_color_helper) public class QDColorHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.square_alpha) View mAlphaView; @BindView(R.id.square_desc_alpha) TextView mAlphaTextView; @BindView(R.id.ratioSeekBar) SeekBar mRatioSeekBar; @BindView(R.id.transformTextView) TextView mTransformTextView; @BindView(R.id.ratioSeekBarWrap) LinearLayout mRatioSeekBarWrap; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_colorhelper, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initContent() { // 设置颜色的 alpha 值 float alpha = 0.5f; int alphaColor = QMUIColorHelper.setColorAlpha(ContextCompat.getColor(getContext(), R.color.colorHelper_square_alpha_background), alpha); mAlphaView.setBackgroundColor(alphaColor); mAlphaTextView.setText(String.format(getResources().getString(R.string.colorHelper_squqre_alpha), alpha)); // 根据比例,在两个 color 值之间计算出一个 color 值 final int fromColor = ContextCompat.getColor(getContext(), R.color.colorHelper_square_from_ratio_background); final int toColor = ContextCompat.getColor(getContext(), R.color.colorHelper_square_to_ratio_background); mRatioSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { int ratioColor = QMUIColorHelper.computeColor(fromColor, toColor, (float) progress / 100); mRatioSeekBarWrap.setBackgroundColor(ratioColor); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); mRatioSeekBar.setProgress(50); // 将 color 颜色值转换为字符串 String transformColor = QMUIColorHelper.colorToString(mTransformTextView.getCurrentTextColor()); mTransformTextView.setText(String.format("这个 TextView 的字体颜色是:%1$s", transformColor)); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDDeviceHelperFragment.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.fragment.util; import androidx.core.content.ContextCompat; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.util.QMUIDeviceHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIDeviceHelper} 的使用示例。 * Created by Kayo on 2016/12/2. */ @Widget(group = Group.Helper, widgetClass = QMUIDeviceHelper.class, iconRes = R.mipmap.icon_grid_device_helper) public class QDDeviceHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private SpannableString getFormatItemValue(CharSequence value) { SpannableString result = new SpannableString(value); result.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.qmui_config_color_gray_5)), 0, value.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return result; } private void initContent() { String isTabletText = booleanToString(QMUIDeviceHelper.isTablet(getContext())); QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(getString(R.string.deviceHelper_tablet_title)), null) .addItemView(mGroupListView.createItemView(getFormatItemValue(String.format("当前设备%1$s平板设备", isTabletText))), null) .addTo(mGroupListView); String isFlymeText = booleanToString(QMUIDeviceHelper.isFlyme()); QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(getString(R.string.deviceHelper_flyme_title)), null) .addItemView(mGroupListView.createItemView(getFormatItemValue(String.format("当前设备%1$s Flyme 系统", isFlymeText))), null) .addTo(mGroupListView); String isMiuiText = booleanToString(QMUIDeviceHelper.isMIUI()); QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(getString(R.string.deviceHelper_miui_title)), null) .addItemView(mGroupListView.createItemView(getFormatItemValue(String.format("当前设备%1$s MIUI 系统", isMiuiText))), null) .addTo(mGroupListView); String isMeizuText = booleanToString(QMUIDeviceHelper.isMeizu()); QMUIGroupListView.newSection(getContext()) .addItemView(mGroupListView.createItemView(getString(R.string.deviceHelper_meizu_title)), null) .addItemView(mGroupListView.createItemView(getFormatItemValue(String.format("当前设备%1$s魅族手机", isMeizuText))), null) .addTo(mGroupListView); } private String booleanToString(boolean b) { return b ? "是" : "不是"; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDDrawableHelperFragment.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.fragment.util; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIDrawableHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIDrawableHelper} 的使用示例。 * Created by Kayo on 2016/12/5. */ @Widget(group = Group.Helper, widgetClass = QMUIDrawableHelper.class, iconRes = R.mipmap.icon_grid_drawable_helper) public class QDDrawableHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.createFromView) QMUIRoundButton mCreateFromViewButton; @BindView(R.id.solidImage) ImageView mSolidImageView; @BindView(R.id.circleGradient) ImageView mCircleGradientView; @BindView(R.id.tintColor) ImageView mTintColorImageView; @BindView(R.id.tintColorOrigin) ImageView mTintColorOriginImageView; @BindView(R.id.separator) View mSeparatorView; private View mRootView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { mRootView = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_drawablehelper, null); ButterKnife.bind(this, mRootView); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initContent(); return mRootView; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initContent() { int commonShapeSize = getResources().getDimensionPixelSize(R.dimen.drawableHelper_common_shape_size); int commonShapeRadius = QMUIDisplayHelper.dp2px(getContext(), 10); // 创建一张指定大小的纯色图片,支持圆角 BitmapDrawable solidImageBitmapDrawable = QMUIDrawableHelper.createDrawableWithSize(getResources(), commonShapeSize, commonShapeSize, commonShapeRadius, ContextCompat.getColor(getContext(), R.color.app_color_theme_3)); mSolidImageView.setImageDrawable(solidImageBitmapDrawable); // 创建一张圆形渐变图片,支持圆角 GradientDrawable gradientCircleGradientDrawable = QMUIDrawableHelper.createCircleGradientDrawable(ContextCompat.getColor(getContext(), R.color.app_color_theme_4), ContextCompat.getColor(getContext(), R.color.qmui_config_color_transparent), commonShapeRadius, 0.5f, 0.5f); mCircleGradientView.setImageDrawable(gradientCircleGradientDrawable); // 设置 Drawable 的颜色 // 创建两张表现相同的图片 BitmapDrawable tintColorBitmapDrawble = QMUIDrawableHelper.createDrawableWithSize(getResources(), commonShapeSize, commonShapeSize, commonShapeRadius, ContextCompat.getColor(getContext(), R.color.app_color_theme_1)); BitmapDrawable tintColorOriginBitmapDrawble = QMUIDrawableHelper.createDrawableWithSize(getResources(), commonShapeSize, commonShapeSize, commonShapeRadius, ContextCompat.getColor(getContext(), R.color.app_color_theme_1)); // 其中一张重新设置颜色 QMUIDrawableHelper.setDrawableTintColor(tintColorBitmapDrawble, ContextCompat.getColor(getContext(), R.color.app_color_theme_7)); mTintColorImageView.setImageDrawable(tintColorBitmapDrawble); mTintColorOriginImageView.setImageDrawable(tintColorOriginBitmapDrawble); // 创建带上分隔线或下分隔线的 Drawable LayerDrawable separatorLayerDrawable = QMUIDrawableHelper.createItemSeparatorBg(ContextCompat.getColor(getContext(), R.color.app_color_theme_7), ContextCompat.getColor(getContext(), R.color.app_color_theme_6), QMUIDisplayHelper.dp2px(getContext(), 2), true); QMUIViewHelper.setBackgroundKeepingPadding(mSeparatorView, separatorLayerDrawable); // 从一个 View 创建 Bitmap mCreateFromViewButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUIDialog.CustomDialogBuilder dialogBuilder = new QMUIDialog.CustomDialogBuilder(getContext()); dialogBuilder.setSkinManager(QMUISkinManager.defaultInstance(getContext())); dialogBuilder.setLayout(R.layout.drawablehelper_createfromview); final QMUIDialog dialog = dialogBuilder.setTitle("示例效果(点击下图关闭本浮层)").create(); ImageView displayImageView = (ImageView) dialog.findViewById(R.id.createFromViewDisplay); Bitmap createFromViewBitmap = QMUIDrawableHelper.createBitmapFromView(mRootView); displayImageView.setImageBitmap(createFromViewBitmap); displayImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); } }); dialog.show(); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDNotchHelperFragment.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.fragment.util; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.Window; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.util.QMUIWindowInsetHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.tab.QMUITab; import com.qmuiteam.qmui.widget.tab.QMUITabBuilder; import com.qmuiteam.qmui.widget.tab.QMUITabSegment; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @Widget(group = Group.Helper, name = "QMUINotchHelper", iconRes = R.mipmap.icon_grid_status_bar_helper) public class QDNotchHelperFragment extends BaseFragment { private static final String TAG = "QDNotchHelperFragment"; @BindView(R.id.not_safe_bg) FrameLayout mNoSafeBgLayout; @BindView(R.id.safe_area_tv) TextView mSafeAreaTv; @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.tabs_container) FrameLayout mTabContainer; @BindView(R.id.tabs) QMUITabSegment mTabSegment; boolean isFullScreen = false; @OnClick(R.id.safe_area_tv) void onClickTv() { if (isFullScreen) { changeToNotFullScreen(); } else { changeToFullScreen(); } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected View onCreateView() { View layout = LayoutInflater.from(getContext()).inflate(R.layout.fragment_notch, null); ButterKnife.bind(this, layout); initTopBar(); initTabs(); QMUIWindowInsetHelper.handleWindowInsets(mTabContainer, WindowInsetsCompat.Type.navigationBars() | WindowInsetsCompat.Type.displayCutout(), true, true ); mNoSafeBgLayout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int height = bottom - top; int width = right - left; int screenUsefulWidth = QMUIDisplayHelper.getUsefulScreenWidth(v); int screenUsefulHeight = QMUIDisplayHelper.getUsefulScreenHeight(v); Log.i(TAG, "width = " + width + "; height = " + height + "; screenUsefulWidth = " + screenUsefulWidth + "; screenUsefulHeight = " + screenUsefulHeight); } }); return layout; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initTabs() { QMUITabBuilder builder = mTabSegment.tabBuilder(); builder.setColorAttr(R.attr.qmui_config_color_gray_6, R.attr.qmui_config_color_blue) .setSelectedIconScale(2f) .setTextSize(QMUIDisplayHelper.sp2px(getContext(), 14), QMUIDisplayHelper.sp2px(getContext(), 16)) .setDynamicChangeIconColor(false); QMUITab component = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_component_selected)) .setText("Components") .build(getContext()); QMUITab util = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_util)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_util_selected)) .setText("Helper") .build(getContext()); QMUITab lab = builder .setNormalDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_lab)) .setSelectedDrawable(ContextCompat.getDrawable(getContext(), R.mipmap.icon_tabbar_lab_selected)) .setText("Lab") .build(getContext()); mTabSegment.addTab(component) .addTab(util) .addTab(lab); mTabSegment.notifyDataChanged(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); changeToFullScreen(); } private void changeToFullScreen() { isFullScreen = true; Activity activity = getActivity(); if (activity != null) { Window window = activity.getWindow(); if (window == null) { return; } View decorView = window.getDecorView(); int systemUi = decorView.getSystemUiVisibility(); systemUi |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; decorView.setSystemUiVisibility(systemUi); QMUIDisplayHelper.setFullScreen(getActivity()); QMUIViewHelper.fadeOut(mTopBar, 300, null, true); QMUIViewHelper.fadeOut(mTabContainer, 300, null, true); } } private void changeToNotFullScreen() { isFullScreen = false; Activity activity = getActivity(); if (activity != null) { Window window = activity.getWindow(); if (window == null) { return; } final View decorView = window.getDecorView(); int systemUi = decorView.getSystemUiVisibility(); systemUi &= ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); systemUi |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(systemUi); QMUIDisplayHelper.cancelFullScreen(getActivity()); QMUIViewHelper.fadeIn(mTopBar, 300, null, true); QMUIViewHelper.fadeIn(mTabContainer, 300, null, true); decorView.post(new Runnable() { @Override public void run() { ViewCompat.requestApplyInsets(decorView); } }); } } @Override protected void popBackStack() { changeToNotFullScreen(); super.popBackStack(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDSpanFragment.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.fragment.util; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import androidx.core.content.ContextCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import com.qmuiteam.qmui.span.QMUIAlignMiddleImageSpan; import com.qmuiteam.qmui.span.QMUIBlockSpaceSpan; import com.qmuiteam.qmui.span.QMUICustomTypefaceSpan; import com.qmuiteam.qmui.span.QMUIMarginImageSpan; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIDrawableHelper; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmuidemo.QDApplication; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * QMUI 中各种 Span 的使用示例。 * Created by Kayo on 2016/12/15. */ @Widget(group = Group.Component, name = "Span", iconRes = R.mipmap.icon_grid_span) public class QDSpanFragment extends BaseFragment { /** * 特殊字体 人民币符号 */ public static Typeface TYPEFACE_RMB; static { try { Typeface tmpRmb = Typeface.createFromAsset(QDApplication.getContext().getAssets(), "fonts/iconfont.ttf"); TYPEFACE_RMB = Typeface.create(tmpRmb, Typeface.NORMAL); } catch (Exception e) { e.printStackTrace(); } } @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.alignMiddle) TextView mAlignMiddleTextView; @BindView(R.id.marginImage) TextView mMarginImageTextView; @BindView(R.id.blockSpace) TextView mBlockSpaceTextView; @BindView(R.id.customTypeface) TextView mCustomTypefaceTextView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_spanhelper, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initContentView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initContentView() { // 支持垂直居中的 ImageSpan int alignMiddleIconLength = QMUIDisplayHelper.dp2px(getContext(), 20); final float spanWidthCharacterCount = 2f; SpannableString spannable = new SpannableString("[icon]" + "这是一行示例文字,前面的 Span 设置了和文字垂直居中并占 " + spanWidthCharacterCount + " 个中文字的宽度"); Drawable iconDrawable = QMUIDrawableHelper.createDrawableWithSize(getResources(), alignMiddleIconLength, alignMiddleIconLength, QMUIDisplayHelper.dp2px(getContext(), 4), ContextCompat.getColor(getContext(), R.color.app_color_theme_3)); if (iconDrawable != null) { iconDrawable.setBounds(0, 0, iconDrawable.getIntrinsicWidth(), iconDrawable.getIntrinsicHeight()); } ImageSpan alignMiddleImageSpan = new QMUIAlignMiddleImageSpan(iconDrawable, QMUIAlignMiddleImageSpan.ALIGN_MIDDLE, spanWidthCharacterCount); spannable.setSpan(alignMiddleImageSpan, 0, "[icon]".length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); mAlignMiddleTextView.setText(spannable); // 支持增加左右间距的 ImageSpan int marginImageLength = QMUIDisplayHelper.dp2px(getContext(), 20); Drawable marginIcon = QMUIDrawableHelper.createDrawableWithSize(getResources(), marginImageLength, marginImageLength, QMUIDisplayHelper.dp2px(getContext(), 4), ContextCompat.getColor(getContext(), R.color.app_color_theme_5)); marginIcon.setBounds(0, 0, marginIcon.getIntrinsicWidth(), marginIcon.getIntrinsicHeight()); CharSequence marginImageTextOne = "左侧内容"; SpannableString marginImageText = new SpannableString(marginImageTextOne + "[margin]右侧内容"); QMUIMarginImageSpan marginImageSpan = new QMUIMarginImageSpan(marginIcon, QMUIAlignMiddleImageSpan.ALIGN_MIDDLE, QMUIDisplayHelper.dp2px(getContext(), 10), QMUIDisplayHelper.dp2px(getContext(), 10)); marginImageText.setSpan(marginImageSpan, marginImageTextOne.length(), marginImageTextOne.length() + "[margin]".length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); mMarginImageTextView.setText(marginImageText); // 整行的空白 Span,可用来用于制作段间距 String paragraphFirst = "这是第一段比较长的段落,演示在段落之间插入段落间距。\n"; String paragraphSecond = "这是第二段比较长的段落,演示在段落之间插入段落间距。"; String spaceString = "[space]"; SpannableString paragraphText = new SpannableString(paragraphFirst + spaceString + paragraphSecond); QMUIBlockSpaceSpan blockSpaceSpan = new QMUIBlockSpaceSpan(QMUIDisplayHelper.dp2px(getContext(), 6)); paragraphText.setSpan(blockSpaceSpan, paragraphFirst.length(), paragraphFirst.length() + spaceString.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); mBlockSpaceTextView.setText(paragraphText); // 自定义部分文字的字体 SpannableString customTypefaceText = new SpannableString(getResources().getString(R.string.spanUtils_rmb) + "100, 前面的人民币符号使用自定义字体特殊处理,对比这个普通的人民币符号: " + getResources().getString(R.string.spanUtils_rmb)); customTypefaceText.setSpan(new QMUICustomTypefaceSpan("", TYPEFACE_RMB), 0, getString(R.string.spanUtils_rmb).length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); mCustomTypefaceTextView.setText(customTypefaceText); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDStatusBarHelperFragment.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.fragment.util; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.util.QMUIStatusBarHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.dialog.QMUITipDialog; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.activity.TranslucentActivity; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIStatusBarHelper} 的使用示例。 * Created by Kayo on 2016/12/12. */ @Widget(group = Group.Helper, widgetClass = QMUIStatusBarHelper.class, iconRes = R.mipmap.icon_grid_status_bar_helper) public class QDStatusBarHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initGroupListView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUIStatusBarHelper.setStatusBarDarkMode(getBaseFragmentActivity()); // 退出界面之前把状态栏还原为白色字体与图标 popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initGroupListView() { QMUIGroupListView.newSection(getContext()) .setDescription("支持 4.4 以上版本的 MIUI 和 Flyme,以及 5.0 以上版本的其他 Android") .addItemView(mGroupListView.createItemView("沉浸式状态栏"), new View.OnClickListener() { @Override public void onClick(View v) { Intent intentTranslucent = new Intent(getContext(), TranslucentActivity.class); startActivity(intentTranslucent); getActivity().overridePendingTransition(R.anim.slide_in_right, R.anim.slide_still); } }) .addTo(mGroupListView); QMUIGroupListView.newSection(getContext()) .setDescription("支持 4.4 以上版本 MIUI 和 Flyme,以及 6.0 以上版本的其他 Android") .addItemView(mGroupListView.createItemView("设置状态栏黑色字体与图标"), new View.OnClickListener() { @Override public void onClick(View v) { QMUIStatusBarHelper.setStatusBarLightMode(getBaseFragmentActivity()); } }) .addItemView(mGroupListView.createItemView("设置状态栏白色字体与图标"), new View.OnClickListener() { @Override public void onClick(View v) { QMUIStatusBarHelper.setStatusBarDarkMode(getBaseFragmentActivity()); } }) .addTo(mGroupListView); QMUIGroupListView.newSection(getContext()) .setDescription("不同机型下状态栏高度可能略有差异,并不是固定值,可以通过这个方法获取实际高度") .addItemView(mGroupListView.createItemView("获取状态栏的实际高度"), new View.OnClickListener() { @Override public void onClick(View v) { String result = String.format(getResources().getString(R.string.statusBarHelper_statusBar_height_result), QMUIStatusBarHelper.getStatusbarHeight(getContext())); final QMUITipDialog tipDialog = new QMUITipDialog.Builder(getContext()).setTipWord(result).create(); tipDialog.show(); mGroupListView.postDelayed(new Runnable() { @Override public void run() { tipDialog.dismiss(); } }, 1500); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDViewHelperAnimationFadeFragment.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.fragment.util; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIViewHelper#fadeIn(View, int, Animation.AnimationListener, boolean)} 与 * {@link QMUIViewHelper#fadeOut(View, int, Animation.AnimationListener, boolean)} 的使用示例。 * Created by Kayo on 2017/2/7. */ @Widget(group = Group.Other, name = "Fade 进退场动画") public class QDViewHelperAnimationFadeFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.actiontBtn) QMUIRoundButton mActionButton; @BindView(R.id.popup) TextView mPopupView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_viewhelper_animation_show_and_hide, null); ButterKnife.bind(this, root); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initContent() { mActionButton.setText("点击显示浮层"); mActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mPopupView.getVisibility() == View.GONE) { mActionButton.setText("点击关闭浮层"); mPopupView.setText("以 Fade 动画显示本浮层"); QMUIViewHelper.fadeIn(mPopupView, 500, null, true); } else { mActionButton.setText("点击显示浮层"); mPopupView.setText("以 Fade 动画隐藏本浮层"); QMUIViewHelper.fadeOut(mPopupView, 500, null, true); } } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDViewHelperAnimationSlideFragment.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.fragment.util; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDirection; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIViewHelper#slideIn(View, int, Animation.AnimationListener, boolean, QMUIDirection)} 与 * {@link QMUIViewHelper#slideOut(View, int, Animation.AnimationListener, boolean, QMUIDirection)} 的使用示例。 * Created by Kayo on 2017/2/7. */ @Widget(group = Group.Other, name = "Slide 进退场动画") public class QDViewHelperAnimationSlideFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.actiontBtn) QMUIRoundButton mActionButton; @BindView(R.id.popup) TextView mPopupView; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_viewhelper_animation_show_and_hide, null); ButterKnife.bind(this, root); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initContent() { mActionButton.setText("点击显示浮层"); mActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mPopupView.getVisibility() == View.GONE) { mActionButton.setText("点击关闭浮层"); mPopupView.setText("以 Slide 动画显示本浮层"); QMUIViewHelper.slideIn(mPopupView, 500, null, true, QMUIDirection.TOP_TO_BOTTOM); } else { mActionButton.setText("点击显示浮层"); mPopupView.setText("以 Slide 动画隐藏本浮层"); QMUIViewHelper.slideOut(mPopupView, 500, null, true, QMUIDirection.BOTTOM_TO_TOP); } } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDViewHelperBackgroundAnimationBlinkFragment.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.fragment.util; import androidx.core.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIViewHelper#playBackgroundBlinkAnimation(View, int)} 的使用示例。 * Created by Kayo on 2017/2/7. */ @Widget(group = Group.Other, name = "做背景闪动动画") public class QDViewHelperBackgroundAnimationBlinkFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.actiontBtn) QMUIRoundButton mActionButton; @BindView(R.id.container) ViewGroup mContainer; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_viewhelper_background_animation, null); ButterKnife.bind(this, root); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initContent() { mActionButton.setText("点击后开始闪动"); mActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUIViewHelper.playBackgroundBlinkAnimation(mContainer, ContextCompat.getColor(getContext(), R.color.app_color_theme_3)); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDViewHelperBackgroundAnimationFullFragment.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.fragment.util; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.roundwidget.QMUIRoundButton; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIViewHelper#playViewBackgroundAnimation(View, int, int, long)} 的使用示例。 * Created by Kayo on 2017/2/7. */ @Widget(group = Group.Other, name = "做背景变化动画") public class QDViewHelperBackgroundAnimationFullFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.actiontBtn) QMUIRoundButton mActionButton; @BindView(R.id.container) ViewGroup mContainer; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_viewhelper_background_animation, null); ButterKnife.bind(this, root); initTopBar(); initContent(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(QDDataManager.getInstance().getName(this.getClass())); } private void initContent() { mActionButton.setText("点击后从黄色背景渐变到绿色背景"); mActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { QMUIViewHelper.playViewBackgroundAnimation(mContainer, ContextCompat.getColor(getContext(), R.color.app_color_theme_3), ContextCompat.getColor(getContext(), R.color.app_color_theme_4), 500); } }); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/util/QDViewHelperFragment.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.fragment.util; import android.view.LayoutInflater; import android.view.View; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.QMUITopBarLayout; import com.qmuiteam.qmui.widget.grouplist.QMUIGroupListView; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; import com.qmuiteam.qmuidemo.manager.QDDataManager; import com.qmuiteam.qmuidemo.model.QDItemDescription; import butterknife.BindView; import butterknife.ButterKnife; /** * {@link QMUIViewHelper} 内各种方法的使用示例。 * Created by Kayo on 2017/02/04. */ @Widget(group = Group.Helper, widgetClass = QMUIViewHelper.class, iconRes = R.mipmap.icon_grid_view_helper) public class QDViewHelperFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; @BindView(R.id.groupListView) QMUIGroupListView mGroupListView; private QDItemDescription mQDItemDescription; @Override protected View onCreateView() { View root = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_grouplistview, null); ButterKnife.bind(this, root); mQDItemDescription = QDDataManager.getInstance().getDescription(this.getClass()); initTopBar(); initContentView(); return root; } private void initTopBar() { mTopBar.addLeftBackImageButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { popBackStack(); } }); mTopBar.setTitle(mQDItemDescription.getName()); } private void initContentView() { QMUIGroupListView.newSection(getContext()) .setTitle("背景动画") .addItemView(mGroupListView.createItemView(QDDataManager.getInstance().getName(QDViewHelperBackgroundAnimationBlinkFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDViewHelperBackgroundAnimationBlinkFragment fragment = new QDViewHelperBackgroundAnimationBlinkFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(QDDataManager.getInstance().getName(QDViewHelperBackgroundAnimationFullFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDViewHelperBackgroundAnimationFullFragment fragment = new QDViewHelperBackgroundAnimationFullFragment(); startFragment(fragment); } }) .addTo(mGroupListView); QMUIGroupListView.newSection(getContext()) .setTitle("进退场动画") .addItemView(mGroupListView.createItemView(QDDataManager.getInstance().getName(QDViewHelperAnimationFadeFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDViewHelperAnimationFadeFragment fragment = new QDViewHelperAnimationFadeFragment(); startFragment(fragment); } }) .addItemView(mGroupListView.createItemView(QDDataManager.getInstance().getName(QDViewHelperAnimationSlideFragment.class)), new View.OnClickListener() { @Override public void onClick(View v) { QDViewHelperAnimationSlideFragment fragment = new QDViewHelperAnimationSlideFragment(); startFragment(fragment); } }) .addTo(mGroupListView); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt ================================================ package com.qmuiteam.qmuidemo.manager import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule @GlideModule class QDAppGlideModule: AppGlideModule() { } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.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.manager; import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.fragment.QDDialogFragment; import com.qmuiteam.qmuidemo.fragment.components.QDBottomSheetFragment; import com.qmuiteam.qmuidemo.fragment.components.QDButtonFragment; import com.qmuiteam.qmuidemo.fragment.components.QDCollapsingTopBarLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDEmptyViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDFloatLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDGroupListViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDLinkTextViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDPopupFragment; import com.qmuiteam.qmuidemo.fragment.components.QDPriorityLinearLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.QDProgressBarFragment; import com.qmuiteam.qmuidemo.fragment.components.QDPullRefreshFragment; import com.qmuiteam.qmuidemo.fragment.components.QDRadiusImageViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDRecyclerViewDraggableScrollBarFragment; import com.qmuiteam.qmuidemo.fragment.components.swipeAction.QDRVSwipeActionFragment; import com.qmuiteam.qmuidemo.fragment.components.QDSliderFragment; import com.qmuiteam.qmuidemo.fragment.components.QDSpanTouchFixTextViewFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTabSegmentFragment; import com.qmuiteam.qmuidemo.fragment.components.QDTipDialogFragment; import com.qmuiteam.qmuidemo.fragment.components.QDVerticalTextViewFragment; import com.qmuiteam.qmuidemo.fragment.components.pullLayout.QDPullFragment; import com.qmuiteam.qmuidemo.fragment.components.qqface.QDQQFaceFragment; import com.qmuiteam.qmuidemo.fragment.components.section.QDSectionLayoutFragment; import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDViewPagerFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDAnimationListViewFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDComposeTipFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScrollFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDEditorFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDEmojiInputFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoClipFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDSchemeFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDSnapHelperFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDWebViewFragment; import com.qmuiteam.qmuidemo.fragment.util.QDColorHelperFragment; import com.qmuiteam.qmuidemo.fragment.util.QDDeviceHelperFragment; import com.qmuiteam.qmuidemo.fragment.util.QDDrawableHelperFragment; import com.qmuiteam.qmuidemo.fragment.util.QDNotchHelperFragment; import com.qmuiteam.qmuidemo.fragment.util.QDSpanFragment; import com.qmuiteam.qmuidemo.fragment.util.QDStatusBarHelperFragment; import com.qmuiteam.qmuidemo.fragment.util.QDViewHelperFragment; import com.qmuiteam.qmuidemo.model.QDItemDescription; import java.util.ArrayList; import java.util.List; /** * @author cginechen * @date 2016-10-21 */ public class QDDataManager { private static QDDataManager _sInstance; private QDWidgetContainer mWidgetContainer; private List> mComponentsNames; private List> mUtilNames; private List> mLabNames; public QDDataManager() { mWidgetContainer = QDWidgetContainer.getInstance(); initComponentsDesc(); initUtilDesc(); initLabDesc(); } public static QDDataManager getInstance() { if (_sInstance == null) { _sInstance = new QDDataManager(); } return _sInstance; } /** * Components */ private void initComponentsDesc() { mComponentsNames = new ArrayList<>(); mComponentsNames.add(QDButtonFragment.class); mComponentsNames.add(QDDialogFragment.class); mComponentsNames.add(QDFloatLayoutFragment.class); mComponentsNames.add(QDEmptyViewFragment.class); mComponentsNames.add(QDTabSegmentFragment.class); mComponentsNames.add(QDProgressBarFragment.class); mComponentsNames.add(QDBottomSheetFragment.class); mComponentsNames.add(QDGroupListViewFragment.class); mComponentsNames.add(QDTipDialogFragment.class); mComponentsNames.add(QDRadiusImageViewFragment.class); mComponentsNames.add(QDVerticalTextViewFragment.class); mComponentsNames.add(QDPullRefreshFragment.class); mComponentsNames.add(QDPopupFragment.class); mComponentsNames.add(QDSpanTouchFixTextViewFragment.class); mComponentsNames.add(QDLinkTextViewFragment.class); mComponentsNames.add(QDQQFaceFragment.class); mComponentsNames.add(QDSpanFragment.class); mComponentsNames.add(QDCollapsingTopBarLayoutFragment.class); mComponentsNames.add(QDViewPagerFragment.class); mComponentsNames.add(QDLayoutFragment.class); mComponentsNames.add(QDPriorityLinearLayoutFragment.class); mComponentsNames.add(QDSectionLayoutFragment.class); mComponentsNames.add(QDContinuousNestedScrollFragment.class); mComponentsNames.add(QDSliderFragment.class); mComponentsNames.add(QDPullFragment.class); mComponentsNames.add(QDRecyclerViewDraggableScrollBarFragment.class); mComponentsNames.add(QDRVSwipeActionFragment.class); } /** * Helper */ private void initUtilDesc() { mUtilNames = new ArrayList<>(); mUtilNames.add(QDColorHelperFragment.class); mUtilNames.add(QDDeviceHelperFragment.class); mUtilNames.add(QDDrawableHelperFragment.class); mUtilNames.add(QDStatusBarHelperFragment.class); mUtilNames.add(QDViewHelperFragment.class); mUtilNames.add(QDNotchHelperFragment.class); } /** * Lab */ private void initLabDesc() { mLabNames = new ArrayList<>(); mLabNames.add(QDAnimationListViewFragment.class); mLabNames.add(QDSnapHelperFragment.class); mLabNames.add(QDArchTestFragment.class); mLabNames.add(QDWebViewFragment.class); mLabNames.add(QDSchemeFragment.class); mLabNames.add(QDComposeTipFragment.class); mLabNames.add(QDPhotoFragment.class); mLabNames.add(QDPhotoClipFragment.class); mLabNames.add(QDEditorFragment.class); mLabNames.add(QDEmojiInputFragment.class); } public QDItemDescription getDescription(Class cls) { return mWidgetContainer.get(cls); } public String getName(Class cls) { QDItemDescription itemDescription = getDescription(cls); if (itemDescription == null) { return null; } return itemDescription.getName(); } public String getDocUrl(Class cls) { QDItemDescription itemDescription = getDescription(cls); if (itemDescription == null) { return null; } return itemDescription.getDocUrl(); } public List getComponentsDescriptions() { List list = new ArrayList<>(); for (int i = 0; i < mComponentsNames.size(); i++) { list.add(mWidgetContainer.get(mComponentsNames.get(i))); } return list; } public List getUtilDescriptions() { List list = new ArrayList<>(); for (int i = 0; i < mUtilNames.size(); i++) { list.add(mWidgetContainer.get(mUtilNames.get(i))); } return list; } public List getLabDescriptions() { List list = new ArrayList<>(); for (int i = 0; i < mLabNames.size(); i++) { list.add(mWidgetContainer.get(mLabNames.get(i))); } return list; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDPreferenceManager.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.manager; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; /** * Created by cgspine on 2018/1/14. */ public class QDPreferenceManager { private static SharedPreferences sPreferences; private static QDPreferenceManager sQDPreferenceManager = null; private static final String APP_VERSION_CODE = "app_version_code"; private static final String APP_SKIN_INDEX = "app_skin_index"; private QDPreferenceManager(Context context) { sPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } public static final QDPreferenceManager getInstance(Context context) { if (sQDPreferenceManager == null) { sQDPreferenceManager = new QDPreferenceManager(context); } return sQDPreferenceManager; } public void setAppVersionCode(int code) { final SharedPreferences.Editor editor = sPreferences.edit(); editor.putInt(APP_VERSION_CODE, code); editor.apply(); } public int getVersionCode() { return sPreferences.getInt(APP_VERSION_CODE, QDUpgradeManager.INVALIDATE_VERSION_CODE); } public void setSkinIndex(int index) { SharedPreferences.Editor editor = sPreferences.edit(); editor.putInt(APP_SKIN_INDEX, index); editor.apply(); } public int getSkinIndex() { return sPreferences.getInt(APP_SKIN_INDEX, QDSkinManager.SKIN_BLUE); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSchemeManager.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.qmuidemo.manager import android.app.Activity import android.util.Log import android.widget.Toast import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandler import com.qmuiteam.qmui.arch.scheme.QMUISchemeHandlerInterceptor import com.qmuiteam.qmui.arch.scheme.QMUISchemeParamValueDecoder import com.qmuiteam.qmui.arch.scheme.SchemeInfo class QDSchemeManager private constructor() { companion object { private const val TAG = "QDSchemeManager" const val SCHEME_PREFIX = "qmui://" @JvmStatic val instance by lazy { QDSchemeManager() } } private val schemeHandler = QMUISchemeHandler.Builder(SCHEME_PREFIX).apply { blockSameSchemeTimeout = 1000 interceptorList.add(object : QMUISchemeHandlerInterceptor { override fun intercept( schemeHandler: QMUISchemeHandler, activity: Activity, schemes: List ): Boolean { // Log the scheme. val sb = StringBuilder() for (scheme in schemes) { sb.append(scheme.origin) sb.append(";") } Log.i(TAG, "handle scheme: $sb") return false } }) interceptorList.add(QMUISchemeParamValueDecoder()) }.build() fun handle(scheme: String): Boolean { if (!schemeHandler.handle(scheme)) { Log.i(TAG, "scheme can not be handled: $scheme") Toast.makeText( QMUISwipeBackActivityManager.getInstance().currentActivity, "scheme can not be handled: $scheme", Toast.LENGTH_SHORT ).show() return false } return true } fun handleMuti(schemes:List): Boolean { if(!schemeHandler.handleSchemes(schemes)){ Log.i(TAG, "scheme can not be handled: ${schemes.joinToString(",")}") Toast.makeText( QMUISwipeBackActivityManager.getInstance().currentActivity, "scheme can not be handled: ${schemes.joinToString(",")}", Toast.LENGTH_SHORT ).show() return false } return true } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDSkinManager.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.manager; import android.content.Context; import android.content.res.Configuration; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmuidemo.QDApplication; import com.qmuiteam.qmuidemo.R; public class QDSkinManager { public static final int SKIN_BLUE = 1; public static final int SKIN_DARK = 2; public static final int SKIN_WHITE = 3; public static void install(Context context) { QMUISkinManager skinManager = QMUISkinManager.defaultInstance(context); skinManager.addSkin(SKIN_BLUE, R.style.app_skin_blue); skinManager.addSkin(SKIN_DARK, R.style.app_skin_dark); skinManager.addSkin(SKIN_WHITE, R.style.app_skin_white); boolean isDarkMode = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; int storeSkinIndex = QDPreferenceManager.getInstance(context).getSkinIndex(); if (isDarkMode && storeSkinIndex != SKIN_DARK) { skinManager.changeSkin(SKIN_DARK); } else if (!isDarkMode && storeSkinIndex == SKIN_DARK) { skinManager.changeSkin(SKIN_BLUE); }else{ skinManager.changeSkin(storeSkinIndex); } } public static void changeSkin(int index) { QMUISkinManager.defaultInstance(QDApplication.getContext()).changeSkin(index); QDPreferenceManager.getInstance(QDApplication.getContext()).setSkinIndex(index); } public static int getCurrentSkin() { return QMUISkinManager.defaultInstance(QDApplication.getContext()).getCurrentSkin(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDUpgradeManager.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.manager; import android.app.Activity; import android.content.Context; /** * Created by cgspine on 2018/1/14. */ public class QDUpgradeManager { public static final int INVALIDATE_VERSION_CODE = 0; public static final int VERSION_1_1_0 = 110; public static final int VERSION_1_1_1 = 111; public static final int VERSION_1_1_2 = 112; public static final int VERSION_1_1_3 = 113; public static final int VERSION_1_1_4 = 114; public static final int VERSION_1_1_5 = 115; public static final int VERSION_1_1_6 = 116; public static final int VERSION_1_1_7 = 117; public static final int VERSION_1_1_8 = 118; public static final int VERSION_1_1_9 = 119; public static final int VERSION_1_1_10 = 1110; public static final int VERSION_1_1_11 = 1111; public static final int VERSION_1_1_12 = 1112; public static final int VERSION_1_2_0 = 120; public static final int VERSION_1_3_1 = 131; public static final int VERSION_1_4_0 = 140; public static final int VERSION_2_0_0_alpha1 = -2001; public static final int VERSION_2_0_0_alpha2 = -2002; public static final int VERSION_2_0_0_alpha3 = -2003; public static final int VERSION_2_0_0_alpha4 = -2004; public static final int VERSION_2_0_0_alpha5 = -2005; public static final int VERSION_2_0_0_alpha6 = -2006; public static final int VERSION_2_0_0_alpha7 = -2007; public static final int VERSION_2_0_0_alpha8 = -2008; public static final int VERSION_2_0_0_alpha9 = -2009; public static final int VERSION_2_0_0_alpha10 = -2010; public static final int VERSION_2_0_0_alpha11 = -2011; public static final int VERSION_2_0_1 = 201; private static final int sCurrentVersion = VERSION_2_0_1; private static QDUpgradeManager sQDUpgradeManager = null; private UpgradeTipTask mUpgradeTipTask; private Context mContext; private QDUpgradeManager(Context context) { mContext = context.getApplicationContext(); } public static final QDUpgradeManager getInstance(Context context) { if (sQDUpgradeManager == null) { sQDUpgradeManager = new QDUpgradeManager(context); } return sQDUpgradeManager; } public void check() { int oldVersion = QDPreferenceManager.getInstance(mContext).getVersionCode(); int currentVersion = sCurrentVersion; boolean versionUpdated = false; if(currentVersion != oldVersion){ if(currentVersion < 0){ // alpha release if(-currentVersion > oldVersion){ versionUpdated = true; } }else if (currentVersion > oldVersion) { versionUpdated = true; } } if(versionUpdated){ if (oldVersion == INVALIDATE_VERSION_CODE) { onNewInstall(currentVersion); } else { onUpgrade(oldVersion, currentVersion); } QDPreferenceManager.getInstance(mContext).setAppVersionCode(currentVersion); } } private void onUpgrade(int oldVersion, int currentVersion) { mUpgradeTipTask = new UpgradeTipTask(oldVersion, currentVersion); } private void onNewInstall(int currentVersion) { mUpgradeTipTask = new UpgradeTipTask(INVALIDATE_VERSION_CODE, currentVersion); } public void runUpgradeTipTaskIfExist(Activity activity) { if (mUpgradeTipTask != null) { mUpgradeTipTask.upgrade(activity); mUpgradeTipTask = null; } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTask.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.manager; public interface UpgradeTask { void upgrade(); } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/UpgradeTipTask.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.manager; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.view.View; import androidx.core.content.ContextCompat; import com.qmuiteam.qmui.skin.QMUISkinManager; import com.qmuiteam.qmui.span.QMUIBlockSpaceSpan; import com.qmuiteam.qmui.span.QMUITouchableSpan; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIPackageHelper; import com.qmuiteam.qmui.util.QMUIViewHelper; import com.qmuiteam.qmui.widget.dialog.QMUIDialog; import com.qmuiteam.qmuidemo.QDMainActivity; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.fragment.components.section.QDSectionLayoutFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScrollFragment; public class UpgradeTipTask implements UpgradeTask { private final int mOldVersion; private final int mNewVersion; public UpgradeTipTask(int oldVersion, int newVersion) { mOldVersion = oldVersion; mNewVersion = newVersion; } @Override public void upgrade() { throw new RuntimeException("please call upgrade(Activity activity)"); } public void upgrade(Activity activity) { String title = String.format(activity.getString(R.string.app_upgrade_tip_title), QMUIPackageHelper.getAppVersion(activity)); CharSequence message = getUpgradeWord(activity); new QMUIDialog.MessageDialogBuilder(activity) .setSkinManager(QMUISkinManager.defaultInstance(activity)) .setTitle(title) .setMessage(message) .create(R.style.ReleaseDialogTheme) .show(); } private void appendBlockSpace(Context context, SpannableStringBuilder builder) { int start = builder.length(); builder.append("[space]"); QMUIBlockSpaceSpan blockSpaceSpan = new QMUIBlockSpaceSpan(QMUIDisplayHelper.dp2px(context, 6)); builder.setSpan(blockSpaceSpan, start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } public CharSequence getUpgradeWord(final Activity activity) { SpannableStringBuilder text = new SpannableStringBuilder(); if(mNewVersion == QDUpgradeManager.VERSION_2_0_1){ text.append("1. Published to MavenCentral.\n"); text.append("2. Updated dep versions.\n"); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha11){ text.append("1. Feature: Added a new widget: QMUINavFragment.\n"); text.append("2. Remove LazyLifecycle, use maxLifecycle for replacement.\n"); text.append("3. Some bug fixes.\n"); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha10){ text.append("1. Feature: Added a new widget: QMUISchemeHandler.\n"); text.append("2. Feature: Supported to remove section title if only one section in QMUIStickSectionAdapter.\n"); text.append("3. Feature: Supported to add a QMUISkinApplyListener to View.\n"); text.append("4. Feature: Add a boolean return value for QMUITabSegment#OnTabClickListener to decide to interrupt the event or not.\n"); text.append("5. Some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha9){ text.append("1. Some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha8){ text.append("1. Feature: Add new widget QMUISeekBar.\n"); text.append("2. Feature: Provide QMUIFragment#registerEffect to replace startFragmentForResult.\n"); text.append("3. Feature: Provide QMUINavFragment to support child fragment navigation\n"); text.append("4. Feature: Refactor swipe back to support muti direction.\n"); text.append("5. Some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha7){ text.append("1. Add OnProgressChangeListener for QMUIProgressBar.\n"); text.append("2. Add skin support for CompoundButton.\n"); text.append("3. Some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha6){ text.append("1. Features: Add new widget QMUITabSegment2 to support ViewPager2.\n"); text.append("2. Remove the skin's default usage.\n"); text.append("3. QMUILayout support radius which is half of the view height or width.\n"); text.append("4. Some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha4){ text.append("1. Features: Add new widget: QMUIPullLayout.\n"); text.append("2. Features: Add new widget: QMUIRVItemSwipeAction.\n"); text.append("3. Support muti instance for QMUISkinManager.\n"); text.append("4. some bug fixes."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha2){ text.append("1. Bugfix: Crash Happened on Android 7 and lower.\n"); text.append("2. Bugfix: QMUIBottomSheet overlapped the navigation bar."); }else if(mNewVersion == QDUpgradeManager.VERSION_2_0_0_alpha1){ text.append("1. Migrated the library to Androidx.\n"); text.append("2. Provided dark mode(skin) support. Almost all widgets are covered.\n"); text.append("3. Refactor some widget such as QMUIPopup, QMUITabSegment. Provided more function.\n"); text.append("4. Provided some simple kotlin methods."); }else if(mNewVersion == QDUpgradeManager.VERSION_1_4_0){ text.append("1. Updated arch library to 0.6.0. Provide annotation MaybeFirstIn and DefaultFirstFragment.\n"); text.append("2. Updated lint library to 1.1.0 to Support Android Studio 3.4+.\n"); text.append("3. Replaced parent theme of QMUI.Compat with Theme.AppCompat.DayNight.\n"); text.append("4. Fixed issues: "); final String[] issues = new String[]{ "636", "642" }; handleIssues(activity, text, issues); }else if(mNewVersion == QDUpgradeManager.VERSION_1_3_1){ text.append("1. "); addNewWidget(activity, text, "QMUIContinuousNestedScrollLayout", QDDataManager.getInstance().getDocUrl(QDContinuousNestedScrollFragment.class)); text.append("\n"); text.append("2. "); addNewWidget(activity, text, "QMUIRadiusImageView2", QDDataManager.getInstance().getDocUrl(QDContinuousNestedScrollFragment.class)); text.append("Implemented with QMUILayout.\n"); text.append("3. Updated arch library to 0.5.0. Fixed issues on new androidx version.\n"); text.append("4. Features: QMUIQQFaceView supports paragraph space when ellipsize at the end.\n"); text.append("5. Features: QMUITabSegment supports space weight.\n"); text.append("6. Features: QMUIPullRefreshLayout added method setToRefreshDirectly().\n"); text.append("7. Fixed issues: "); final String[] issues = new String[]{ "562", "563", "563" }; handleIssues(activity, text, issues); } else if (mNewVersion == QDUpgradeManager.VERSION_1_2_0) { text.append("1. "); addNewWidget(activity, text, "QMUIStickySectionLayout", QDDataManager.getInstance().getDocUrl(QDSectionLayoutFragment.class)); text.append("\n"); text.append("2. Supported startFragmentForResult in child fragment. #499"); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_12) { text.append("1. Fixed drag issues when refreshing.\n"); text.append("2. Fixed the crash in QMUIPopup under Android 4.4 because of webp."); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_11) { text.append("1. Updated arch library to 0.3.0. Now developer must update support library to 28 or use androidx.\n"); text.append("2. Feature: Added custom typeface support in QMUITabSegment.\n"); text.append("3. Fixed a bug that QMUICollapsingTopBarLayout will lose title if swipe back.\n"); text.append("4. Fixed a bug that span click event is not triggered in QMUIQQFaceView. #473\n"); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_10) { text.append("1. Simplified the use of QMUIWebContainer.\n"); text.append("2. Refactored QMUITabSegment to handle operations such as reducing item.\n"); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_9) { text.append("1. Fixed an error that fitSystemWindows does not work in QMUIWebContainer.\n"); text.append("2. Fixed an error that swiping back would blink.\n"); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_8) { text.append("1. Implemented QMUIWebView (beta), where supports for env(safe-area-inset-*) in css were added.\n"); text.append("2. Feature: QMUIQQFaceView supports gravity(left/right/center-horizontal) attribute.\n"); text.append("3. Feature: allows setting shadow color on Android ROM version 9 and higher.\n"); text.append("4. Feature: allows control of the size of left icon in QMUIGroupListView.Section by calling the method setLeftIconSize.\n"); text.append("5. Feature: supports custom web url matcher in QMUILinkify.\n"); text.append("6. Fixed some bugs and increased code robustness."); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_7) { text.append("1. Improved QMUINotchHelper to support Xiaomi. \n"); text.append("2. Improved drawing effect of QMUIQQFaceView. \n"); text.append("3. Fixed a bug where UI would become unresponsive " + "if popBackStack was invoked during fragment transitions."); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_6) { text.append("1. Feature: QMUINotchHelper, a new helper class for notch compatibility. \n"); appendBlockSpace(activity, text); text.append("2. Added \"more\" click event to QMUIQQFaceView.\n"); appendBlockSpace(activity, text); text.append("3. Added text color setter for QMUITouchableSpan.\n"); appendBlockSpace(activity, text); text.append("4. The method startFragmentAndDestroyCurrent in QMUIFragment supports transfer of target fragment.\n"); appendBlockSpace(activity, text); text.append("5. Fixed issues: "); final String[] issues = new String[]{ "334", "352" }; handleIssues(activity, text, issues); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_5) { text.append("1. Code optimization for QMUIDialog.\n"); appendBlockSpace(activity, text); text.append("2. Added a return value to KeyboardVisibilityEventListener, which " + "determines whether OnGlobalLayoutListener is deleted.\n"); appendBlockSpace(activity, text); text.append("3. Bug fix: getSignCount() in QMUITabSegment should return 0 " + "if view is not visible.\n"); appendBlockSpace(activity, text); text.append("4. Bug fix: fixed incorrect layout of translucent status bar may " + "appear in Android 4.4.\n"); appendBlockSpace(activity, text); text.append("5. Fixed issues: "); final String[] issues = new String[]{ "304", "308" }; handleIssues(activity, text, issues); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_4) { text.append("1. Added a new widget: QMUIPriorityLinearLayout.\n"); appendBlockSpace(activity, text); text.append("2. Bug fix: marginRight does not make sense for controlling " + "the position of signCount, it should use marginLeft.\n"); appendBlockSpace(activity, text); text.append("3. Fixed issues: "); final String[] issues = new String[]{ "165", "247" }; handleIssues(activity, text, issues); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_3) { text.append("1. Feature: delay validation of QMUIFragment.canDragBack() until a pop " + "gesture occurs. This feature allows you to control pop gesture on the fly.\n"); appendBlockSpace(activity, text); text.append("2. Replace QMUIMaterialProgressDrawable with CircularProgressDrawable, " + "an official implementation.\n"); appendBlockSpace(activity, text); text.append("3. Fixed issues: "); final String[] issues = new String[]{ "254", "258", "284", "285", "293", "294" }; handleIssues(activity, text, issues); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_2) { text.append("1. Updated arch library to 0.0.4 to fix issue #235.\n"); appendBlockSpace(activity, text); text.append("2. Added API to get line count in QMUIFloatLayout"); } else if (mNewVersion == QDUpgradeManager.VERSION_1_1_1) { text.append("1. Bug fixes: can not read /system/build.prop begin from android 8.0.\n"); appendBlockSpace(activity, text); text.append("2. Allow custom layout in QMUIPopup."); } else if (mNewVersion <= QDUpgradeManager.VERSION_1_1_0) { text.append("1. Added QMUILayout, making it easy to implement shadows, radii, and separators.\n"); appendBlockSpace(activity, text); text.append("2. Refactored the theme usage of QMUITopbar.\n"); appendBlockSpace(activity, text); text.append("3. Refactored QMUIDialog for more flexible configuration.\n"); appendBlockSpace(activity, text); text.append("4. Updated arch library to 0.0.3 to provide methods runAfterAnimation and startFragmentForResult.\n"); appendBlockSpace(activity, text); text.append("5. Bug fixes: "); final String[] issues = new String[]{ "125", "127", "132", "141", "177", "184", "198", "200", "209", "213" }; handleIssues(activity, text, issues); } else { text.append("welcome to QMUI!"); } return text; } private void addNewWidget(final Activity activity, SpannableStringBuilder text, final String widgetName, final String docUrl) { text.append("Added a new widget: "); if (docUrl == null || docUrl.length() == 0) { text.append(widgetName); } else { int start = text.length(); text.append(widgetName); int end = text.length(); text.setSpan(new QMUITouchableSpan(QMUIViewHelper.getActivityRoot(activity), R.attr.app_skin_span_normal_text_color, R.attr.app_skin_span_pressed_text_color, 0, 0) { @Override public void onSpanClick(View widget) { Intent intent = QDMainActivity.createWebExplorerIntent(activity, docUrl, widgetName); activity.startActivity(intent); } }, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } text.append("."); } private void handleIssues(final Activity activity, SpannableStringBuilder text, String[] issues) { final String issueBaseUrl = "https://github.com/Tencent/QMUI_Android/issues/"; int start, end; for (int i = 0; i < issues.length; i++) { if (i == issues.length - 1) { text.append("and "); } final String issue = issues[i]; start = text.length(); text.append("#"); text.append(issue); end = text.length(); int normalColor = ContextCompat.getColor(activity, R.color.app_color_blue); int pressedColor = ContextCompat.getColor(activity, R.color.app_color_blue_pressed); text.setSpan(new QMUITouchableSpan(normalColor, pressedColor, 0, 0) { @Override public void onSpanClick(View widget) { Intent intent = QDMainActivity.createWebExplorerIntent(activity, issueBaseUrl + issue, null); activity.startActivity(intent); } }, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if (i < issues.length - 1) { text.append(", "); } else { text.append("."); } } } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/model/CustomEffect.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.model; import com.qmuiteam.qmui.arch.effect.Effect; public class CustomEffect extends Effect { private final String mContent; public CustomEffect(String content){ mContent = content; } public String getContent() { return mContent; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/model/QDItemDescription.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.model; import com.qmuiteam.qmuidemo.base.BaseFragment; /** * @author cginechen * @date 2016-10-21 */ public class QDItemDescription { private Class mKitDemoClass; private String mKitName; private int mIconRes; private String mDocUrl; public QDItemDescription(Class kitDemoClass, String kitName){ this(kitDemoClass, kitName, 0, ""); } public QDItemDescription(Class kitDemoClass, String kitName, int iconRes, String docUrl) { mKitDemoClass = kitDemoClass; mKitName = kitName; mIconRes = iconRes; mDocUrl = docUrl; } public Class getDemoClass() { return mKitDemoClass; } public String getName() { return mKitName; } public int getIconRes() { return mIconRes; } public String getDocUrl() { return mDocUrl; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/model/SectionHeader.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.model; import com.qmuiteam.qmui.widget.section.QMUISection; public class SectionHeader implements QMUISection.Model { private final String text; public SectionHeader(String text){ this.text = text; } public String getText() { return text; } @Override public SectionHeader cloneForDiff() { return new SectionHeader(getText()); } @Override public boolean isSameItem(SectionHeader other) { return text == other.text || (text != null && text.equals(other.text)); } @Override public boolean isSameContent(SectionHeader other) { return true; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/model/SectionItem.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.model; import com.qmuiteam.qmui.widget.section.QMUISection; public class SectionItem implements QMUISection.Model { private final String text; public SectionItem(String text){ this.text = text; } public String getText() { return text; } @Override public SectionItem cloneForDiff() { return new SectionItem(getText()); } @Override public boolean isSameItem(SectionItem other) { return text == other.text || (text != null && text.equals(other.text)); } @Override public boolean isSameContent(SectionItem other) { return true; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDLoadingItemView.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.view; import android.content.Context; import android.graphics.Color; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.Gravity; import android.widget.FrameLayout; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.widget.QMUILoadingView; public class QDLoadingItemView extends FrameLayout { private QMUILoadingView mLoadingView; public QDLoadingItemView(@NonNull Context context) { this(context, null); } public QDLoadingItemView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); mLoadingView = new QMUILoadingView(context, QMUIDisplayHelper.dp2px(context, 24), Color.LTGRAY); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); lp.gravity = Gravity.CENTER; addView(mLoadingView, lp); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( QMUIDisplayHelper.dp2px(getContext(), 48), MeasureSpec.EXACTLY)); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mLoadingView.start(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mLoadingView.stop(); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDSectionHeaderView.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.view; import android.content.Context; import android.graphics.Color; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.Gravity; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmuidemo.R; import com.qmuiteam.qmuidemo.model.SectionHeader; public class QDSectionHeaderView extends LinearLayout { private TextView mTitleTv; private ImageView mArrowView; private int headerHeight = QMUIDisplayHelper.dp2px(getContext(), 56); public QDSectionHeaderView(Context context) { this(context, null); } public QDSectionHeaderView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); setOrientation(LinearLayout.HORIZONTAL); setGravity(Gravity.CENTER_VERTICAL); setBackgroundColor(Color.WHITE); int paddingHor = QMUIDisplayHelper.dp2px(context, 24); mTitleTv = new TextView(getContext()); mTitleTv.setTextSize(20); mTitleTv.setTextColor(Color.BLACK); mTitleTv.setPadding(paddingHor, 0, paddingHor, 0); addView(mTitleTv, new LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); mArrowView = new AppCompatImageView(context); mArrowView.setImageDrawable(QMUIResHelper.getAttrDrawable(getContext(), R.attr.qmui_common_list_item_chevron)); mArrowView.setScaleType(ImageView.ScaleType.CENTER); addView(mArrowView, new LinearLayout.LayoutParams(headerHeight, headerHeight)); } public ImageView getArrowView() { return mArrowView; } public void render(SectionHeader header, boolean isFold) { mTitleTv.setText(header.getText()); mArrowView.setRotation(isFold ? 0f : 90f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(headerHeight, MeasureSpec.EXACTLY)); } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDShadowAdjustLayout.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.view; import android.content.Context; import androidx.customview.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.qmuiteam.qmui.layout.QMUIFrameLayout; import com.qmuiteam.qmuidemo.R; /** * Created by cgspine on 2018/3/22. */ public class QDShadowAdjustLayout extends QMUIFrameLayout { ViewDragHelper viewDragHelper; public QDShadowAdjustLayout(Context context) { this(context, null); } public QDShadowAdjustLayout(Context context, AttributeSet attrs) { super(context, attrs); viewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child.getId() == R.id.layout_for_test; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } @Override public int clampViewPositionVertical(View child, int top, int dy) { return top; } }); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return viewDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { viewDragHelper.processTouchEvent(event); return true; } } ================================================ FILE: qmuidemo/src/main/java/com/qmuiteam/qmuidemo/view/QDWebView.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.view; import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.webkit.WebSettings; import com.qmuiteam.qmui.util.QMUIDisplayHelper; import com.qmuiteam.qmui.util.QMUIPackageHelper; import com.qmuiteam.qmui.util.QMUIResHelper; import com.qmuiteam.qmui.widget.webview.QMUIWebView; import com.qmuiteam.qmuidemo.BuildConfig; import com.qmuiteam.qmuidemo.R; /** * Created by cgspine on 2017/12/5. */ public class QDWebView extends QMUIWebView { public QDWebView(Context context) { this(context, null); } public QDWebView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.webViewStyle); } public QDWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @SuppressLint("SetJavaScriptEnabled") protected void init(Context context) { WebSettings webSettings = getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setSupportZoom(true); webSettings.setBuiltInZoomControls(true); webSettings.setDefaultTextEncodingName("GBK"); webSettings.setUseWideViewPort(true); webSettings.setLoadWithOverviewMode(true); webSettings.setDomStorageEnabled(true); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setTextZoom(100); webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); String screen = QMUIDisplayHelper.getScreenWidth(context) + "x" + QMUIDisplayHelper.getScreenHeight(context); String userAgent = "QMUIDemo/" + QMUIPackageHelper.getAppVersion(context) + " (Android; " + Build.VERSION.SDK_INT + "; Screen/" + screen + "; Scale/" + QMUIDisplayHelper.getDensity(context) + ")"; String agent = getSettings().getUserAgentString(); if (agent == null || !agent.contains(userAgent)) { getSettings().setUserAgentString(agent + " " + userAgent); } // 开启调试 if (BuildConfig.DEBUG) { setWebContentsDebuggingEnabled(true); } } public void exec(final String jsCode) { evaluateJavascript(jsCode, null); } @Override protected int getExtraInsetTop(float density) { return (int) (QMUIResHelper.getAttrDimen(getContext(), R.attr.qmui_topbar_height) / density); } } ================================================ FILE: qmuidemo/src/main/res/color/s_app_color_blue_2.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_app_color_blue_3.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_app_color_blue_to_red.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_app_color_gray.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_app_color_gray_dark.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_btn_blue.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_btn_gray.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/color/s_topbar_btn_color.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_popup_close_dark.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_popup_close_with_bg_dark.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_quick_action_copy.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_quick_action_delete_line.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_quick_action_dict.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_quick_action_line.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/icon_quick_action_share.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/launcher_bg.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/pager_layout_item_bg.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/s_app_touch_fix_area_bg.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/s_list_item_bg_dark_1.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/s_list_item_bg_dark_2.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/tab_panel_bg.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable/web_explorer_progress.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/drawable-night/launcher_bg.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/activity_arch_test.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/activity_translucent.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/drawablehelper_createfromview.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_about.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_animation_listview.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_arch_test.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_button.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_collapsing_topbar_layout.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_colorhelper.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_continuous_nested_scroll.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_drawablehelper.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_emptyview.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_floatlayout.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_fsw_viewpager.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_grouplistview.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_home.xml ================================================ ================================================ FILE: qmuidemo/src/main/res/layout/fragment_layout.xml ================================================